JavaScript 中的傅里叶变换可视化

Hex*_*ler 2 javascript fft typescript canvasjs ionic2

我使用这个库对音频文件进行 fft,之后我想用canvasjs可视化结果,但我不知道如何做到这一点。

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

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

编辑:我尝试重现这个,但我得到了以下结果。幅度并没有那么糟糕,但相位却很可怕。我认为这Math.atan2()将是问题所在,因为这是根据两个数字计算的,所以我尝试使用 Math.js 和数组,但得到了相同的结果。(链接中的预期结果)

    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++;
    }
Run Code Online (Sandbox Code Playgroud)

我完全迷路了,请给我一些建议。

在此输入图像描述

enh*_*lep 5

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

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

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

这是输出的一对图片(光谱缩放至 50%):

Rx787赛车转速波形 在此输入图像描述

需要注意的是,5 分钟的录音不需要实时播放以获得样本精确的显示,而 webkit 音频路径 (a) 需要与声音文件播放一样长的时间for 或 (b) 会因使用 AudioWorkers 时丢帧而导致输出损坏(使用 Chrome 版本 57.20.2987.98 x64)

我为了实现这个而浪费了几天/几周的时间——希望你能原谅我一些混乱/冗余的代码!

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;
}
Run Code Online (Sandbox Code Playgroud)

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