javascript中的傅里叶变换可视化

javascript中的傅里叶变换可视化,javascript,typescript,ionic2,fft,canvasjs,Javascript,Typescript,Ionic2,Fft,Canvasjs,我过去常常对音频文件进行fft,之后我想用它来可视化结果,但我不知道怎么做 我不确定应该使用什么作为x和y轴。如果是频率和振幅,怎么做?最大x轴值应等于最大频率,如果是,则步长值是多少?(我计算了震级和最大频率) 如果有人能帮忙,我将不胜感激 编辑: 我试图复制,但我得到了以下结果。震级没有那么糟糕,但相位很可怕。我认为Math.atan2()将是问题所在,因为这是从两个数字计算出来的,所以我尝试了Math.js和数组,但得到了相同的结果。(链接中的预期结果) (变量i=0;i

我过去常常对音频文件进行fft,之后我想用它来可视化结果,但我不知道怎么做

我不确定应该使用什么作为
x
y
轴。如果是频率和振幅,怎么做?最大
x
轴值应等于最大频率,如果是,则步长值是多少?(我计算了震级和最大频率)

如果有人能帮忙,我将不胜感激

编辑: 我试图复制,但我得到了以下结果。震级没有那么糟糕,但相位很可怕。我认为
Math.atan2()
将是问题所在,因为这是从两个数字计算出来的,所以我尝试了Math.js和数组,但得到了相同的结果。(链接中的预期结果)

(变量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)); } //阶段 计数器=0; 对于(var i=0;i 我完全迷路了,请给我一些建议


当从服务器运行以下代码时(本地主机可以),可以避免尝试从
文件提供服务时遇到的跨源问题://
url

我已经阅读了webkit音频的规范,并用javascript重新实现了
getByteFreqData
。这允许处理音频文件,而不必使用(损坏的)AudioWorkers实现(这可能已经修复,我已经有一段时间没有重新检查)

通常,时间由X轴表示,频率由Y轴表示,任何一个格中的频率强度由绘制的像素强度表示-您可以选择任何调色板。我忘了我是从哪里得到灵感的——也许是来自《无畏的代码》,也许是来自我在某处看到的某个webkit音频演示——不知道

这是一对输出图片(光谱比例为50%):

需要注意的是,5分钟的录音不需要实时播放,以获得准确的样本显示,而webkit音频路由要么(a)与声音文件播放的时间一样长,要么(b)在使用AudioWorkers(使用Chrome版本57.20.2987.98 x64)时由于帧丢失而导致输出中断

我在实现这一点上浪费了几天/几周的时间-希望你能原谅我一些混乱/冗余的代码

1.fft.js
“严格使用”;
函数ajaxGetArrayBuffer(url、onLoad、onError)
{
var ajax=new-XMLHttpRequest();
ajax.onload=function(){onload(this);}//function(){onload(this);}
ajax.onerror=function(){console.log(“ajax请求失败:”+url);onerror(this);}
open(“GET”,url,true);
ajax.responseType='arraybuffer';
ajax.send();
}
var complex_t=函数(实,imag)
{
this.real=real;
this.imag=imag;
归还这个;
}
complex_t.prototype.toString=函数()
{
返回“”;
}
complex_t.prototype.scalarDiv=函数(标量)
{
this.real/=标量;
this.imag/=标量;
归还这个;
}
//返回一个复杂值数组
函数dft(复数数组)
{
var nSamples=complexArray.length;
var结果=[];

对于(var outidex=0;outidex当以下代码从服务器运行时(localhost很好),可以避免尝试从
文件提供服务时遇到的跨源问题://
url

我已经阅读了webkit audio的规范,并在javascript中重新实现了
getByteFreqData
。这允许处理音频文件,而不必使用(损坏的)AudioWorkers实现(这可能已经修复,我很久没有重新检查过)

通常,时间由X轴表示,频率由Y轴表示,频率的强度由绘制的像素强度表示——你可以选择任何调色板。我忘了我从哪里得到灵感的了——也许是来自Audacity代码,也许是来自some webkit音频演示我在某处看到过-不知道

这是一对输出图片(光谱比例为50%):

需要注意的是,5分钟的录音不需要实时播放,以获得准确的样本显示,而webkit音频路由要么(a)与声音文件播放的时间一样长,要么(b)在使用AudioWorkers(使用Chrome版本57.20.2987.98 x64)时由于帧丢失而导致输出中断

我在实现这一点上浪费了几天/几周的时间-希望你能原谅我一些混乱/冗余的代码

1.fft.js
“严格使用”;
函数ajaxGetArrayBuffer(url、onLoad、onError)
{
var ajax=new-XMLHttpRequest();
ajax.onload=function(){onload(this);}//function(){onload(this);}
ajax.onerror=function(){console.log(“ajax请求失败:”+url);onerror(this);}
open(“GET”,url,true);
ajax.responseType='arraybuffer';
ajax.send();
}
var complex_t=函数(实,imag)
{
this.real=real;
this.imag=imag;
归还这个;
}
complex_t.prototype.toString=函数()
{
返回“”;
}
complex_t.prototype.scalarDiv=函数(标量)
{
this.real/=标量;
this.imag/=标量;
归还这个;
}
//返回一个复杂值数组
函数dft(复数数组)
{
var nSamples=complexArray.length;
var结果=[];

对于(var outidex=0;outidex,请在下面找到原始任务中链接到的Matlab页面上显示的可视化实现
    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++;
    }
"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;
}
<!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>