Search code examples
web-audio-api

Web audio API - LFO with 'triangle' shape connected to biquadFilter clicks


I have an LFO connected to a biquadFilter.

When the LFO gain is greater than the current biquadFilter.frequency.value, it results in notice-able clicking of audio.

Is there a way prevent this clicking ?

The biquadFilter has an envelope on it, so the frequency sweeps up and down after a note on the keyboard is pressed.

This is what I think is happening :

LFO cutting through envelope value.

This is what I'd like to happen : LFO respecting the filter.

Here is a minimal example : http://codepen.io/js-bs/pen/bgyZPx

var audioContext = new AudioContext();
var masterGain = audioContext.createGain()
masterGain.connect(audioContext.destination)
masterGain.gain.value = .3;

// Filter
var filter = audioContext.createBiquadFilter();
filter.type = 'lowpass'
filter.frequency.value = 0;
filter.connect(masterGain);

// Oscillator
var osc = audioContext.createOscillator();
osc.frequency.value = 440;
osc.type = 'square';
osc.start();
osc.connect(filter);

// LFO
var lfo = this.audioContext.createOscillator();
var lfoGain = this.audioContext.createGain();
lfo.type = 'triangle';
lfo.start();
lfo.connect(lfoGain);
lfo.frequency.value = 5;
lfoGain.gain.value = 10000;
lfoGain.connect(filter.frequency);

document.addEventListener('click', function(e){
  if(e.target.id==='lfo' && e.target.checked){
    lfo.connect(lfoGain);
  } else if (e.target.id==='lfo') {
    lfo.disconnect();
  }
})

document.addEventListener('mousedown', filterEnvelopeOn);
document.addEventListener('mouseup', filterEnvelopeOff);

function filterEnvelopeOn () {
  let now = audioContext.currentTime;
  let frequency = filter.frequency;
  let attack = 0.5;
  let decay = 0.4;
  let sustain = 200;

  let freq = 10000;
  frequency.cancelScheduledValues(0)
  frequency.setValueAtTime(60, now)
  frequency.linearRampToValueAtTime(freq, now + attack)
  frequency.linearRampToValueAtTime(sustain, now + attack + decay)
}

 function filterEnvelopeOff () {
   filter.frequency.cancelScheduledValues(0);
   let now = audioContext.currentTime;
   let frequency = filter.frequency;
   let release = 0.1;
   frequency.cancelScheduledValues(0);
   frequency.setValueAtTime(frequency.value, now);
   frequency.linearRampToValueAtTime(0, now + release);
  }

Solution

  • If I change my codepen example code from :

    lfoGain.connect(filter.frequency);

    to :

    lfoGain.connect(filter.detune);

    The audible clicking is gone, and the synth sounds as I intend it to.

    If anyone can explain why this works that would be helpful and I'll mark your answer as accepted.

    When you have AudioNodes connected to an AudioParam, that AudioParam takes its value as all of the outputs from the AudioNodes and its own value added together. In your case, the values output by your lfoGain range from -10000 to +10000, because the output of the oscillator ranges from -1 to 1, and those values are then multiplied by your lfoGain.gain value.

    When you have your lfoGain connected to the frequency, you're telling the frequency to oscillate between 0Hz and +20000Hz (10000 ± 10000) immediately after the attack of your envelope, between -9800Hz and +10200Hz (200 ± 10000) at your sustain, and between -10000Hz and +10000Hz ( 0 ± 10000 ) after the release of your envelope.

    When you instead have your lfoGain connected to the detune, the computed frequency will oscillate proportionally to the frequency value you set with the envelope.

    computedFrequency(t) = frequency(t) * pow(2, detune(t) / 1200)

    You're now telling the detune value to oscillate between -10000cents and +10000cents (0 ± 10000 ), and this oscillation, rather than adding to the frequency, multiplies it by a value that's approximately between 1/323 and 323.

    Immediately after the envelope's attack, the minimum computed frequency in the oscillation is approximately 31Hz, at the sustain, the minimum is approximately 0.62Hz, and after the release of your envelope, the computed frequency range is between 0 and 0 (0/323, 0*323).

    This is at least what it's being told to do, actual implementation may be different.