admin管理员组文章数量:1402291
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 puted 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.
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 puted 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.
Share Improve this question edited Jun 7, 2017 at 8:52 Vishwas R 3,4301 gold badge19 silver badges40 bronze badges asked Apr 27, 2017 at 14:12 HexilerHexiler 2032 silver badges12 bronze badges2 Answers
Reset to default 5When the following code is run from a server (localhost is fine) one avoids the cross-origin problems encountered when trying to serve from a file:///
url.
I've read the specs for webkit audio and re-implemented getByteFreqData
in javascript. This allows an audio file to be processed without having to use the (broken) AudioWorkers implementation (this may have been fixed by now, I've not re-checked in quite some time)
Typically, time is represented by the X-axis, frequency is represented by the Y-axis and intensity of the frequencieies in any one bin are represented by the intensity of the pixels drawn - you can choose any palette you wish. I forget where I got the inspiration for the one used - perhaps it was from the code of Audacity, perhaps it was from some webkit Audio demo I saw somewhere - dunno.
Here's a pair of pictures of the output (spectrum is scaled to 50%):
The thing to note is that a 5 minute recording soesn't need to be played through in real-time in order to get a sample-accurate display, whereas the webkit audio route either (a) takes as long as the sound-file plays for or (b) gives you a broken output due to dropped frames when using AudioWorkers (using Chrome Version 57.20.2987.98 x64)
I lost days/weeks of my life implementing this - hopefully you'll forgive me some messy/redundant code!
1. fft.js
"use strict";
function ajaxGetArrayBuffer(url, onLoad, onError)
{
var ajax = new XMLHttpRequest();
ajax.onload = function(){onLoad(this);} //function(){onLoad(this);}
ajax.onerror = function(){console.log("ajax request failed to: "+url);onError(this);}
ajax.open("GET",url,true);
ajax.responseType = 'arraybuffer';
ajax.send();
}
var plex_t = function(real, imag)
{
this.real = real;
this.imag = imag;
return this;
}
plex_t.prototype.toString = function()
{
return "<"+this.real + " " + this.imag + "j>";
}
plex_t.prototype.scalarDiv = function(scalar)
{
this.real /= scalar;
this.imag /= scalar;
return this;
}
// returns an array of plex values
function dft( plexArray )
{
var nSamples = plexArray.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 += plexArray[inIndex].real*Math.cos(angle) + plexArray[inIndex].imag*Math.sin(angle);
//sumImag += -plexArray[inIndex].real*Math.sin(angle) + plexArray[inIndex].imag*Math.cos(angle);
sumReal += plexArray[inIndex].real*cosA + plexArray[inIndex].imag*sinA;
sumImag += -plexArray[inIndex].real*sinA + plexArray[inIndex].imag*cosA;
}
result.push( new plex_t(sumReal, sumImag) );
}
return result;
}
function inverseDft( plexArray )
{
var nSamples = plexArray.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 += plexArray[inIndex].real*Math.cos(angle) + plexArray[inIndex].imag*Math.sin(angle);
//sumImag += -plexArray[inIndex].real*Math.sin(angle) + plexArray[inIndex].imag*Math.cos(angle);
sumReal += plexArray[inIndex].real*cosA / nSamples
+ plexArray[inIndex].imag*sinA / nSamples;
}
result.push( new plex_t(sumReal, 0) );
}
return result;
}
function FFT(plexArray,isForwards) //double *x,double *y)
{
var n,i,i1,j,k,i2,l,l1,l2; // long
var c1,c2,tx,ty,t1,t2,u1,u2,z; // double
var m = Math.log2( plexArray.length );
if (Math.floor(m) != m)
return false;
// Calculate the number of points
//n = 1;
//for (i=0;i<m;i++)
// n *= 2;
n = plexArray.length;
// Do the bit reversal
i2 = n >> 1;
j = 0;
for (i=0; i<n-1; i++)
{
if (i < j)
{
tx = plexArray[i].real; //x[i];
ty = plexArray[i].imag; //y[i];
plexArray[i].real = plexArray[j].real; //x[i] = x[j];
plexArray[i].imag = plexArray[j].imag; //y[i] = y[j];
plexArray[j].real = tx; //x[j] = tx;
plexArray[j].imag = ty; //y[j] = ty;
}
k = i2;
while (k <= j)
{
j -= k;
k >>= 1;
}
j += k;
}
// Compute the FFT
c1 = -1.0;
c2 = 0.0;
l2 = 1;
for (l=0; l<m; l++)
{
l1 = l2;
l2 <<= 1;
u1 = 1.0;
u2 = 0.0;
for (j=0; j<l1; j++)
{
for (i=j; i<n; i+=l2)
{
i1 = i + l1;
t1 = u1*plexArray[i1].real - u2*plexArray[i1].imag; //t1 = u1 * x[i1] - u2 * y[i1];
t2 = u1*plexArray[i1].imag + u2*plexArray[i1].real; //t2 = u1 * y[i1] + u2 * x[i1];
plexArray[i1].real = plexArray[i].real-t1; //x[i1] = x[i] - t1;
plexArray[i1].imag = plexArray[i].imag-t2; //y[i1] = y[i] - t2;
plexArray[i].real += t1; //x[i] += t1;
plexArray[i].imag += t2; //y[i] += t2;
}
z = u1 * c1 - u2 * c2;
u2 = u1 * c2 + u2 * c1;
u1 = z;
}
c2 = Math.sqrt((1.0 - c1) / 2.0);
if (isForwards == true)
c2 = -c2;
c1 = Math.sqrt((1.0 + c1) / 2.0);
}
// Scaling for forward transform
if (isForwards == true)
{
for (i=0; i<n; i++)
{
plexArray[i].real /= n; //x[i] /= n;
plexArray[i].imag /= n; //y[i] /= n;
}
}
return true;
}
/*
BlackmanWindow
alpha = 0.16
a0 = (1-alpha)/2
a1 = 1 / 2
a2 = alpha / 2
func(n) = a0 - a1 * cos( 2*pi*n / N ) + a2 * cos(4*pi*n/N)
*/
function applyBlackmanWindow( floatSampleArray )
{
let N = floatSampleArray.length;
let alpha = 0.16;
let a0 = (1-alpha)/2;
let a1 = 1 / 2;
let a2 = alpha / 2;
var result = [];
for (var n=0; n<N; n++)
result.push( (a0 - (a1 * Math.cos( 2*Math.PI*n / N )) + (a2 * Math.cos(4*Math.PI*n/N)) ) * floatSampleArray[n]);
return result;
}
// function(n) = n
//
function applyRectWindow( floatSampleArray )
{
var result = [], N = floatSampleArray.length;
for (var n=0; n<N; n++)
result.push( floatSampleArray[n] );
return result;
}
// function(n) = 1/2 (1 - cos((2*pi*n)/N))
//
function applyHanningWindow( floatSampleArray )
{
var result = [], N=floatSampleArray.length, a2=1/2;
for (var n=0; n<N; n++)
result.push( a2 * (1 - Math.cos( (2*Math.PI*n)/N)) * floatSampleArray[n] );
return result;
}
function convertToDb( floatArray )
{
var result = floatArray.map( function(elem) { return 20 * Math.log10(elem); } );
return result;
}
var lastFrameBins = [];
function getByteFreqData( floatSampleArray )
{
var windowedData = applyBlackmanWindow(floatSampleArray.map(function(elem){return elem;}) );
// var windowedData = applyRectWindow(floatSampleArray.map(function(elem){return elem;}) );
// var windowedData = applyHanningWindow(floatSampleArray.map(function(elem){return elem;}) );
var plexSamples = windowedData.map( function(elem) { return new plex_t(elem,0); } );
FFT(plexSamples, true);
var timeConst = 0.80;
var validSamples = plexSamples.slice(plexSamples.length/2);
var validBins = validSamples.map( function(el){return Math.sqrt(el.real*el.real + el.imag*el.imag);} );
if (lastFrameBins.length != validBins.length)
{
console.log('lastFrameBins refresh');
lastFrameBins = [];
validBins.forEach( function() {lastFrameBins.push(0);} );
}
var smoothedBins = [];
smoothedBins = validBins.map(
function(el, index)
{
return timeConst * lastFrameBins[index] + (1-timeConst)*el;
}
);
lastFrameBins = smoothedBins.slice();
var bins = convertToDb( smoothedBins );
var minDB = -100;
var maxDB = -30;
bins = bins.map(
function(elem)
{
if (isNaN(elem)==true)
elem = minDB;
else if (elem < minDB)
elem = minDB;
else if (elem > maxDB)
elem = maxDB;
return ((elem-minDB) / (maxDB-minDB) ) * 255;
}
);
return bins;
}
2. offlineAudioContext.html
<!doctype html>
<html>
<head>
<script>
"use strict";
function newEl(tag){return document.createElement(tag)}
function newTxt(txt){return document.createTextNode(txt)}
function byId(id){return document.getElementById(id)}
function allByClass(clss,parent){return (parent==undefined?document:parent).getElementsByClassName(clss)}
function allByTag(tag,parent){return (parent==undefined?document:parent).getElementsByTagName(tag)}
function toggleClass(elem,clss){elem.classList.toggle(clss)}
function addClass(elem,clss){elem.classList.add(clss)}
function removeClass(elem,clss){elem.classList.remove(clss)}
function hasClass(elem,clss){elem.classList.contains(clss)}
// useful for HtmlCollection, NodeList, String types
function forEach(array, callback, scope){for (var i=0,n=array.length; i<n; i++)callback.call(scope, array[i], i, array);} // passes back stuff we need
// callback gets data via the .target.result field of the param passed to it.
function loadFileObject(fileObj, loadedCallback){var a = new FileReader();a.onload = loadedCallback;a.readAsDataURL( fileObj );}
function ajaxGetArrayBuffer(url, onLoad, onError)
{
var ajax = new XMLHttpRequest();
ajax.onload = function(){onLoad(this);} //function(){onLoad(this);}
ajax.onerror = function(){console.log("ajax request failed to: "+url);onError(this);}
ajax.open("GET",url,true);
ajax.responseType = 'arraybuffer';
ajax.send();
}
function ajaxGet(url, onLoad, onError)
{
var ajax = new XMLHttpRequest();
ajax.onload = function(){onLoad(this);}
ajax.onerror = function(){console.log("ajax request failed to: "+url);onError(this);}
ajax.open("GET",url,true);
ajax.send();
}
function ajaxPost(url, phpPostVarName, data, onSucess, onError)
{
var ajax = new XMLHttpRequest();
ajax.onload = function(){ onSucess(this);}
ajax.onerror = function() {console.log("ajax request failed to: "+url);onError(this);}
ajax.open("POST", url, true);
ajax.setRequestHeader("Content-type","application/x-www-form-urlencoded");
ajax.send(phpPostVarName+"=" + encodeURI(data) );
}
function ajaxPostForm(url, formElem, onSuccess, onError)
{
var formData = new FormData(formElem);
ajaxPostFormData(url, formData, onSuccess, onError)
}
function ajaxPostFormData(url, formData, onSuccess, onError)
{
var ajax = new XMLHttpRequest();
ajax.onload = function(){onSuccess(this);}
ajax.onerror = function(){onError(this);}
ajax.open("POST",url,true);
ajax.send(formData);
}
function getTheStyle(tgtElement)
{
var result = {}, properties = window.getComputedStyle(tgtElement, null);
forEach(properties, function(prop){result[prop] = properties.getPropertyValue(prop);});
return result;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
window.addEventListener('load', onDocLoaded, false);
function onDocLoaded(evt)
{
// analyseAudioOnline('3 seconds.wav');
// analyseAudioOnline('closer.wav');
// analyseAudioOffline( 'closer.wav');
// onlineScriptAnalyse( '8bit 8363hz.wav', 512*8 );
// analyseAudioOffline( '8bit 8363hz.wav' );
// graphAudioFile( 'Sneaky Sound System - I Love It (Riot In Belgium Forest Rave Mix).mp3' );
// graphAudioFile( '56chevy.wav' );
// graphAudioFile( '56chevy.wav' );
// graphAudioFile( 'birds.mp3' );
// graphAudioFile( 'closer.wav' );
// graphAudioFile( 'Speeding-car-horn_doppler_effect_sample.ogg' );
// graphAudioFile( 'test.music.wav' );
// graphAudioFile( '787b_1.mp3' );
// graphAudioFile( '787b_2.mp3' );
graphAudioFile( '787b_4.mp3' );
// graphAudioFile( 'Blur_-_Girls_&_Boys.ogg' );
// graphAudioFile( '3 seconds.wav' );
// graphAudioFile( '01 - Van Halen - 1984 - 1984.mp3' );
// graphAudioFile( 'rx8.mp3' );
// graphAudioFile( 'sa22c_1m.mp3' );
// graphAudioFile( 'Lily is Gone.mp4.MP3' );
//onlineScriptAnalyse( '8bit 8363hz.wav' );
//onlineScriptAnalyse( '100smokes2.wav' );
};
const FFTSIZE = 1024*2;
function graphAudioFile( url )
{
var audioCtx = new(window.AudioContext || window.webkitAudioContext)();
ajaxGetArrayBuffer(url, onAjaxLoaded);
function onAjaxLoaded(ajax)
{
audioCtx.decodeAudioData(ajax.response, onDataDecoded);
}
function onDataDecoded(buffer)
{
var startTime = performance.now();
var samples = buffer.getChannelData(0);
var tgtCanvas = byId('wavCanvas');
tgtCanvas.width = samples.length/(FFTSIZE);
tgtCanvas.samples = samples;
// tgtCanvas.onclick = onCanvasClicked;
tgtCanvas.addEventListener('click', onCanvasClicked, false);
function onCanvasClicked(evt)
{
playSound(this.samples, buffer.sampleRate, 100);
}
drawFloatWaveform(samples, buffer.sampleRate, byId('wavCanvas') );//canvas)
var fftSize = FFTSIZE;
var offset = 0;
let spectrumData = [];
var numFFTs = Math.floor(samples.length / FFTSIZE);
var curFFT = 0;
var progElem = byId('progress');
while (offset+fftSize < samples.length)
{
let curFrameSamples = samples.slice(offset, fftSize+offset);
offset += fftSize;
let bins = getByteFreqData( curFrameSamples );
bins.reverse();
spectrumData.push( bins );
curFFT++;
}
drawFreqData(spectrumData);
var endTime = performance.now();
console.log("Calculation/Drawing time: " + (endTime-startTime) );
}
}
function playSound(inBuffer, sampleRate, vol) // floatSamples [-1..1], 44100, 0-100
{
var audioCtx = new(window.AudioContext || window.webkitAudioContext)();
var ctxBuffer = audioCtx.createBuffer(1, inBuffer.length, sampleRate);
var dataBuffer = ctxBuffer.getChannelData(0);
dataBuffer.forEach( function(smp, i) { dataBuffer[i] = inBuffer[i]; } );
var source = audioCtx.createBufferSource();
source.buffer = ctxBuffer;
source.gain = 1 * vol/100.0;
source.connect(audioCtx.destination);
source.onended = function()
{
//drawFreqData(result);
source.disconnect(audioCtx.destination);
//processor.disconnect(audioCtx.destination);
};
source.start(0);
}
function drawFloatWaveform(samples, sampleRate, canvas)
{
var x,y, i, n = samples.length;
var dur = (n / sampleRate * 1000)>>0;
canvas.title = 'Duration: ' + dur / 1000.0 + 's';
var width=canvas.width,height=canvas.height;
var ctx = canvas.getContext('2d');
ctx.strokeStyle = 'yellow';
ctx.fillStyle = '#303030';
ctx.fillRect(0,0,width,height);
ctx.moveTo(0,height/2);
ctx.beginPath();
for (i=0; i<n; i++)
{
x = (i*width) / n;
y = (samples[i]*height/2)+height/2;
ctx.lineTo(x, y);
}
ctx.stroke();
ctx.closePath();
}
var binSize;
function onlineScriptAnalyse(url, fftSize)
{
var audioCtx = new(window.AudioContext || window.webkitAudioContext)();
ajaxGetArrayBuffer(url, onAjaxLoaded);
function onAjaxLoaded(ajax)
{
audioCtx.decodeAudioData(ajax.response, onDataDecoded);
}
function onDataDecoded(buffer)
{
var ctxBuffer = audioCtx.createBuffer(1, buffer.length, buffer.sampleRate);
var dataBuffer = ctxBuffer.getChannelData(0);
// dataBuffer.forEach( function(smp, i) { dataBuffer[i] = inBuffer[i]; } );
console.log(dataBuffer);
var analyser = audioCtx.createAnalyser();
var source = audioCtx.createBufferSource();
// source.getChannelData
if (fftSize != undefined)
analyser.fftSize = fftSize;
else
analyser.fftSize = 1024;
source.buffer = buffer;
source.connect(analyser);
source.connect(audioCtx.destination);
source.onended = function()
{
drawFreqData(result);
source.disconnect(processor);
processor.disconnect(audioCtx.destination);
}
console.log(buffer);
console.log('length: ' + buffer.length);
console.log('sampleRate: ' + buffer.sampleRate);
console.log('fftSize: ' + analyser.fftSize);
console.log('nFrames: ' + Math.floor( buffer.length / analyser.fftSize) );
console.log('binBandwidth: ' + (buffer.sampleRate / analyser.fftSize).toFixed(3) );
binSize = buffer.sampleRate / analyser.fftSize;
var result = [];
var processor = audioCtx.createScriptProcessor(analyser.fftSize, 1, 1);
processor.connect(audioCtx.destination);
processor.onaudioprocess = function(e)
{
var data = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(data);
result.push( data );
}
source.connect(processor);
source.start(0);
}
}
function analyseAudioOnline(url)
{
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
ajaxGetArrayBuffer(url, onAjaxLoaded);
function onAjaxLoaded(ajax)
{
audioCtx.decodeAudioData(ajax.response, onDataDecoded);
}
function onDataDecoded(buffer)
{
var analyser = audioCtx.createAnalyser();
var source = audioCtx.createBufferSource()
source.buffer = buffer;
source.connect(analyser);
source.connect(audioCtx.destination);
var nFftSamples = 2048;
analyser.fftSize = nFftSamples;
var bufferLength = analyser.frequencyBinCount;
let result = [], isdone=false;
source.onended = function()
{
console.log('audioCtx.onplete firing');
isdone = true;
drawFreqData(result);
};
function copyCurResult()
{
if (isdone == false)
{
let copyVisual = requestAnimationFrame(copyCurResult);
}
var dataArray = new Uint8Array(bufferLength);
analyser.getByteFrequencyData(dataArray);
result.push( dataArray );
console.log(dataArray.length);
}
source.start(0);
copyCurResult();
}
}
function analyseAudioOffline(url)
{
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
ajaxGetArrayBuffer(url, onAjaxLoaded);
function onAjaxLoaded(ajax)
{
audioCtx.decodeAudioData(ajax.response, onDataDecoded);
}
function onDataDecoded(buffer)
{
let nFftSamples = 512;
var result = [];
var offlineCtx = new OfflineAudioContext(buffer.numberOfChannels,buffer.length,buffer.sampleRate);
var processor = offlineCtx.createScriptProcessor(nFftSamples, 1, 1);
// processor.bufferSize = nFftSamples;
processor.connect(offlineCtx.destination);
var analyser = offlineCtx.createAnalyser();
analyser.fftSize = nFftSamples;
analyser.connect(processor);
offlineCtx.onplete =
function()
{
// console.log('plete');
// console.log(result);
// drawFreqData(result);
console.log(result);
};
// offlineCtx.startRendering();
processor.onaudioprocess = function(e)
{
var wavData = new Float32Array(analyser.fftSize);
analyser.getFloatTimeDomainData(wavData);
//var data = new Uint8Array(analyser.frequencyBinCount);
//analyser.getByteFrequencyData(data);
result.push( wavData ); //data );
}
var source = offlineCtx.createBufferSource();
source.buffer = buffer;
source.start(0);
source.connect(offlineCtx.destination);
source.connect(analyser);
offlineCtx.startRendering();
/*
source = context.createBufferSource();
source.connect(analyser
*/
//console.log(offlineCtx);
}
}
function pixel(x,y, imgData, r,g,b)
{
let index = ((y*imgData.width)+x) * 4;
imgData.data[index + 0] = r;
imgData.data[index + 1] = g;
imgData.data[index + 2] = b;
imgData.data[index + 3] = 255;
}
function getPixelColor(val)
{
// var result = [255,255,255];
// return result;
return [val,val,val];
}
function getColHsl(val)
{
let result = [0,0,0];
if (val != 0)
{
var span = newEl('span');
span.style.backgroundColor = "hsl(" + Math.floor( (val/255)*360) + ", 100%, 50%)";
//var col = span.style.backgroundColor;
//col = col.replace(/[a-z]*\(* *\)*/g, ''); // all lower-case, (, [space], )
//col = col.split(',');
var col = span.style.backgroundColor.replace(/[a-z]*\(* *\)*/g, '').split(',');
result[0] = col[0];
result[1] = col[1];
result[2] = col[2];
}
return result;
}
var colTable = [];
function getColHsl2(val)
{
if (colTable.length == 0)
{
for (var i=0; i<256; i++)
colTable.push( getColHsl(i) );
}
return colTable[val>>0];
}
function drawFreqData(dataArray)
{
console.log( "num fft samples: " + dataArray.length );
var canvas = newEl('canvas');
var canCtx = canvas.getContext('2d');
var horizScale = 1;
canvas.width = dataArray.length*horizScale;
canvas.height = dataArray[0].length;
canCtx.clearRect(0,0,canvas.width,canvas.height);
let imgData = canCtx.getImageData(0,0,canvas.width,canvas.height);
canCtx.lineWidth = 1;
canCtx.strokeStyle = 'rgba(0, 0, 0, 0)';
for (var curX=0; curX<canvas.width/horizScale; curX++)
{
var curMax = dataArray[curX][0];
var curMaxIndex = 0;
for (var curY=0; curY<canvas.height; curY++)
{
var curVal = dataArray[curX][curY];
if (curVal > curMax)
{
curMax = curVal;
curMaxIndex = curY;
}
//let rgb = getPixelColor(curVal);
let rgb = getColHsl2(curVal);
pixel(curX, canvas.height-curY-1, imgData, rgb[0],rgb[1],rgb[2]); //255,255,255); //curVal,curVal);
}
pixel(curX, canvas.height-curMaxIndex-1, imgData, 0,230,255);
}
canCtx.putImageData(imgData, 0, 0);
document.body.appendChild(canvas);
}
</script>
<style>
canvas
{
border: solid 4px red;
/* height: 512px; */
}
</style>
<script src='fft.js'></script>
</head>
<body>
<div>Generating: <span id='progress'></span>%</div>
<canvas id='wavCanvas' width=2048 height=256></canvas><br>
</body>
</html>
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 ments. 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 puted 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 puting 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 bees immediately apparent. ;)
var plex_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 plex (no pun intended!) - result the same
// returns an array of plex values
function dft( plexArray )
{
var nSamples = plexArray.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 += plexArray[inIndex].real*Math.cos(angle) + plexArray[inIndex].imag*Math.sin(angle);
//sumImag += -plexArray[inIndex].real*Math.sin(angle) + plexArray[inIndex].imag*Math.cos(angle);
sumReal += plexArray[inIndex].real*cosA + plexArray[inIndex].imag*sinA;
sumImag += -plexArray[inIndex].real*sinA + plexArray[inIndex].imag*cosA;
}
result.push( new plex_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 inplete. Amongst other short-ings, 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 plexSamples = [];
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 plex array from these samples - the real part are the samples' values
// the plex part is all 0
samples.forEach( function(sampleReal, index, srcArray){ plexSamples[index] = new plex_t(sampleReal, 0); } );
// do an fft on them
var fftSamples = dft( plexSamples );
// pute and graph the magnitude
var magnitude = [];
fftSamples.forEach(
function(plexValue, index)
{
magnitude[index] = Math.sqrt( (plexValue.real*plexValue.real) + (plexValue.imag*plexValue.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);
// pute and graph the phase
var phase = [];
fftSamples.forEach(
function(plexValue, index)
{
phase[index] = rad2deg( Math.atan2(plexValue.imag, plexValue.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>
本文标签: typescriptFourier transform visualization in javascriptStack Overflow
版权声明:本文标题:typescript - Fourier transform visualization in javascript - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1744351974a2602105.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论