2

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.

enter image description here

Vishwas R
  • 3,340
  • 1
  • 16
  • 37
Hexiler
  • 213
  • 2
  • 12

2 Answers2

4

When 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%):

Waveform of Rx787 race-car revving enter image description here

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 complex_t = function(real, imag)
{
    this.real = real;
    this.imag = imag;
    return this;
}

complex_t.prototype.toString = function()
{
    return "<"+this.real + " " + this.imag + "j>";
}

complex_t.prototype.scalarDiv = function(scalar)
{
    this.real /= scalar;
    this.imag /= scalar;
    return this;
}

// 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 inverseDft( 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 / nSamples
                     + complexArray[inIndex].imag*sinA / nSamples;
        }
        result.push( new complex_t(sumReal, 0) );
    }
    return result;
}

function FFT(complexArray,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( complexArray.length );
   if (Math.floor(m) != m)
    return false;

   // Calculate the number of points
   //n = 1;
   //for (i=0;i<m;i++) 
   //   n *= 2;
   n = complexArray.length;

   // Do the bit reversal
   i2 = n >> 1;
   j = 0;
   for (i=0; i<n-1; i++) 
   {
      if (i < j)
      {
        tx = complexArray[i].real;  //x[i];
        ty = complexArray[i].imag;  //y[i];
        complexArray[i].real = complexArray[j].real;    //x[i] = x[j];
        complexArray[i].imag = complexArray[j].imag;    //y[i] = y[j];
        complexArray[j].real = tx;  //x[j] = tx;
        complexArray[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*complexArray[i1].real - u2*complexArray[i1].imag;   //t1 = u1 * x[i1] - u2 * y[i1];
            t2 = u1*complexArray[i1].imag + u2*complexArray[i1].real;   //t2 = u1 * y[i1] + u2 * x[i1];
            complexArray[i1].real = complexArray[i].real-t1;    //x[i1] = x[i] - t1; 
            complexArray[i1].imag = complexArray[i].imag-t2;    //y[i1] = y[i] - t2;
            complexArray[i].real += t1; //x[i] += t1;
            complexArray[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++)
      {
         complexArray[i].real /= n; //x[i] /= n;
         complexArray[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 complexSamples = windowedData.map( function(elem) { return   new complex_t(elem,0); } );
    FFT(complexSamples, true);
    var timeConst = 0.80;

    var validSamples = complexSamples.slice(complexSamples.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.oncomplete 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.oncomplete = 
        function()
        {
        //  console.log('complete');
        //  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>
enhzflep
  • 12,927
  • 2
  • 32
  • 51
  • Thanks for your reply, actually I use TypeScript (Ionic 2), but I will check it, how can I adapt there. I attached two images, i would like a similar result. Is that possible? – Hexiler Apr 27 '17 at 14:54
  • You're welcome. Ahhhh, gotcha.(Yes, definitely possible) In light of the images you've linked to, you'd perhaps be better off having a look at the Javascript spectrum analyzer discussed in the following page: http://www.arc.id.au/SpectrumAnalyser.html – enhzflep Apr 27 '17 at 15:05
  • Maybe you have any idea, what am I doing wrong in my context? – Hexiler Apr 27 '17 at 15:27
  • Not sure what you're actually doing, much less so which parts of it are conducive to the desired output and which of the things you're doing are wrong. Could you edit your initial question to include a complete example, along with the output it produces? **if** the first image is someone else's, and the 2nd is yours - recall that only 1/2 of the bins contain useful information, since the FFT also computes negative frequencies (which mirror the positive ones) - so you have to discard 1/2 of the results. It's a _large_ topic. Consider the symmetry discussed in http://www.arc.id.au/ZoomFFT.html – enhzflep Apr 27 '17 at 15:44
  • I checked your links, and some other. For example [this](https://www.mathworks.com/help/matlab/math/fourier-transforms.html), and i reproduced the example (with a little bit worse result), so now i see what should i get, but it use `t = 0:1/50:10-1/50;` instead of the `50` i should use the samplerate, so instead of the `10-1/50` i need `i < array.length - 1/samplerate`. (**Edit**: Btw i edited the main question too. :) ) – Hexiler Apr 28 '17 at 09:10
  • I edited the question with more specification, @enhzflep can you check it please? – Hexiler May 02 '17 at 16:24
  • @Helixer - sure. I'll look into it later today when I have some time. I certainly can't afford MATLAB at the moment, but downloaded (the $0) Octave when I was working with FFTs and Convolution on audio files - it helped me heaps. It may or may not be of use to you too. It's almost entirely compatible with MATLAB scripts ;) – enhzflep May 02 '17 at 22:30
  • I installed matlab yesterday and helps a little. Perhaps i need a better javascript libraryfor fft transform, becasue mine gives very bad result now, and i do not understand why. At least i can check my result in matlab. I am very gladful for your help. (Some weeks ago i got a mirrored result, but my teacher said that shouldnt be mirrored, however i cheked matlab fft and that is mirrored too, so i have to read more to understand. i only want a "simple" fourier visualization :D :( ) – Hexiler May 03 '17 at 10:00
  • Cool. I do note that your code above appears to be missing the FFT step. You're creating the buffer of raw samples and then computing the phase and magnitude arrays from them, rather than the FFT of them. The mirroring thing is to do with negative frequencies - but I don't have the knowledge or recollection required at the moment to explain it much better than this (I'm entirely self-taught and even then only for an idle passing curiosity in the topic) I'll keep playing tonight if I'm not needed for home duties later. ;) – enhzflep May 04 '17 at 02:56
  • I was bad i used fft like `transform(realArray, imag)` just somehow i forgot to write it in the question. However i found some problem... I finally could reproduce an fft + phase too, on a random list `var d = [1.2352, -2.235235, 3.235325, 4.124141, -5.124, 6.1241]`. So the fourier is good, but the phase was buggy first,. Because Matlab use `+0.0000000i` instead of `-4.121412e-15` so it will be `180` however, in `js` it's something negative, so it will be `-180`. So i changed the "bad" values to `+0` but it is disgusting, and still not work on the array what i mention in the main question. – Hexiler May 04 '17 at 07:15
2

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>
enhzflep
  • 12,927
  • 2
  • 32
  • 51
  • When i used `1/50` that disturbed the calculations, i get the good result, thank you very much. Howerver, when i implemented this to my audio data, it gives me the samre result what i got a some weeks ago, so it is ok, i know yet. I am grateful for your help and kindness thanks :D I hope with a little formatting, slicing i can finish my thesis work :D – Hexiler May 05 '17 at 08:45
  • :grins: Great, you're most welcome. Hmmm. The spectrum of an audio signal is much harder to be sure about than one of a generated signal, hence the suggestion to use Matlab or Octave. We all stand on the shoulders of those that came before us. Hopefully when you've finished your thesis, you'll be in a position to give someone a leg-up too! :) – enhzflep May 10 '17 at 03:17