javascript 中的傅里叶变换可视化

Fourier transform visualization in javascript

我用this library to do an fft on an audio file, after this I want to visualize the result with canvasjs,但我不知道怎么做。

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

如果有人能提供帮助,我将不胜感激。

编辑: 我尝试重现 this,但我得到了以下结果。幅度还不错,但相位太可怕了。我认为 Math.atan2() 会是问题所在,因为它是根据两个数字计算的,所以我尝试使用 Math.js 和数组,但得到了相同的结果。 (预期结果在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++;
    }

我完全迷路了,请给我一些帮助。

当以下代码 运行 来自服务器(本地主机很好)时,可以避免尝试从 file:/// url 提供服务时遇到的跨域问题。

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

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

这是一对输出图片(频谱缩放到 50%):

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

实现这个我失去了 days/weeks 的生命 - 希望你能原谅我一些 messy/redundant 代码!

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>

请在下面找到原始问题中链接到的 Matlab 页面上显示的可视化的实现。

我在我之前的评论之一中重新实现了来自 Sprectrum 分析器的图形绘制代码的部分功能。我从来没有抽出时间处理 y 轴上的标签和输出的缩放比例,但这对我来说并不重要,因为我们真正谈论的是可视化和用于创建它们的基础数据仍然忠实于此由 Matlab 和 Octave 计算 - 特别注意我必须对第二和第三图中显示的数据进行归一化这一事实。我编写代码最初是为了在 FFT 的帮助下执行两个音频信号的卷积的各个步骤中可视化数据以提高速度。 (为了简洁起见,我在这里包含了 DFT 代码)

另请注意,您正在使用浮点加法来确定生成样本时的当前时间。这意味着当你完成计算时,你将累积接近 500 次错误,这就是你必须编写的原因 for (var i = 0; i < 10 - 1/50; i+=1/50) 而不是 for (var i = 0; i < 10; i+=1/50)

更好的方法是将当前步数乘以每步之间的间隔,就像我在 fillSampleBuffer 中所做的那样 - 这样可以确保您不会累积浮点错误。如果您在循环的每次迭代中检查 currentTime,差异就会立即显现出来。 ;)

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>