Linear Filters with WebAudio API

 October 30, 2018    Antonin Novak
education    education_signals    




Click here to start the audio:

Select filter-type:



f = 1000 Hz
Q = 0
G = 0 dB









'use srict';

// Web Audio API part
let file_buffer;
let source_node;

const audio_context = new (window.AudioContext || window.webkitAudioContext)();
const playButton = document.querySelector('#play');
const stopButton = document.querySelector('#stop');
const type_of_filter = document.querySelector('#type_of_filter_selector');

const filter = audio_context.createBiquadFilter();
filter.type = 'lowpass';
filter.Q.value = 0;
filter.frequency.value = 1000;
filter.gain.value = 0;
filter.connect(audio_context.destination);

window.fetch('../../audio_sample.wav')
.then(response => response.arrayBuffer())
.then(arrayBuffer => audio_context.decodeAudioData(arrayBuffer))
.then(audioBuffer => {
  playButton.disabled = false;
  file_buffer = audioBuffer;
});

playButton.onclick = () => play(file_buffer);
stopButton.onclick = () => stop();

function play(audioBuffer) {
  source_node = audio_context.createBufferSource();
  source_node.buffer = audioBuffer;
  source_node.loop = true;
  source_node.connect(filter);
  source_node.start();
  playButton.disabled = true;
  stopButton.disabled = false;
};

function stop(){
  source_node.stop();
  playButton.disabled = false;
  stopButton.disabled = true;
};


// Frequency Repsonse Function
const FRF_frequency_axes = logspace(1, 4, 500);
let magResponseOutput = new Float32Array(FRF_frequency_axes.length);
let phaseResponseOutput = new Float32Array(FRF_frequency_axes.length);

let FRF_magnitude = []; 
let FRF_phase = []; 
for(let i = 0; i < FRF_frequency_axes.length; i++){
  FRF_magnitude[i] = 0;
  FRF_phase[i] = 0;
}




// Plotly figure
Plotly.newPlot('mydiv1', [{
        x: FRF_frequency_axes,
        y: FRF_magnitude,
        type: 'line'
    },{
        x: FRF_frequency_axes,
        y: FRF_phase,
        type: 'line',
        yaxis: 'y2'
    }], {
        xaxis: {
            title: 'Frequency [Hz]',
            type: 'log',
            range: [1, 4]
        },
        yaxis: {
            title: 'Magnitude [dB]',
            range: [-20, 20],
            titlefont: {color: 'rgb(30, 120, 180)'},
            tickfont: {color: 'rgb(30, 120, 180)'}            
        },
        yaxis2: {
            title: 'Phase [deg]',
            range: [-200, 200],
            overlaying: 'y',
            titlefont: {color: 'rgb(255, 127, 14)'},
            tickfont: {color: 'rgb(255, 127, 14)'},            
            side: 'right'            
        },
        margin: {l: 60, r: 60, b: 40, t: 20, pad: 1},
        showlegend: false
    }, {
        displayModeBar: false, 
        responsive: true
    }
);


// prepare the data
calcFrequencyResponse();
const id_divQ = document.querySelector('#div_Q');
const id_divG = document.querySelector('#div_G');
id_divG.style.display = "none";


// Callback functions changeFilter (select menu)
changeFilter = function(value){
    filter.type = value;
    calcFrequencyResponse();

    // divQ display ?
    if (['lowshelf','highshelf'].includes(value)) {
        id_divQ.style.display = "none";
    } else {
        id_divQ.style.display = "block";
    }
    
    // divG display ?
    if (['lowpass','highpass','bandpass','allpass'].includes(value)) {
        id_divG.style.display = "none";
    } else {
        id_divG.style.display = "block";
    }
    
    // set Q limits
    if (['lowpass','highpass'].includes(value)) {
        document.getElementById("ChangeQSlider").min = -10;
        document.getElementById("ChangeQSlider").value = 0;
        changeQ(0);
    } else {
        document.getElementById("ChangeQSlider").min = 0;
        document.getElementById("ChangeQSlider").value = 1;
        changeQ(1);
    }
    
}

// Callback functions changeFreq (slider)
changeFreq = function(Freq) {
  document.getElementById("ChangeFreqValue").innerHTML = "F = " + Freq.toString() + " Hz";
  filter.frequency.value = Freq;
  calcFrequencyResponse();
}

// Callback functions changeQ (slider)
changeQ = function(Q) {
  document.getElementById("ChangeQValue").innerHTML = "Q = " + Q.toString();
  filter.Q.value = Q;
  calcFrequencyResponse();
}

// Callback functions changeG (slider)
changeG = function(G) {
  document.getElementById("ChangeGValue").innerHTML = "G = " + G.toString() + " dB";
  filter.gain.value = G;
  calcFrequencyResponse();
}


function calcFrequencyResponse() {
    filter.getFrequencyResponse(FRF_frequency_axes, magResponseOutput, phaseResponseOutput);

    for(let i = 0; i < FRF_frequency_axes.length; i++){
        FRF_magnitude[i] = 20*Math.log10( Math.abs( magResponseOutput[i] ) );
        FRF_phase[i] = 180/Math.PI*phaseResponseOutput[i];
    }

    //Plotly.restyle('mydiv1', 'y', [FRF_magnitude]);    
    Plotly.animate('mydiv1', {
        data: [{y: FRF_magnitude},{y: FRF_phase}],
        traces: [0,1],
        layout: {}
    }, {
        transition: {
            duration: 500,
            easing: 'cubic-in-out'
        },
        frame: {
          duration: 500
        }
    })    
}


function logspace(from, to, num) {
    let a = (to-from)/(num-1);
    let data =  [...Array(num).keys()].map(x => Math.pow(10,a*x + from));
    return new Float32Array(data);
}