Volume control with Web Audio API

I’m working on a simple project to create an instrument, using Web Audio API, and wrote the following snippet (you can press ‘Q‘ to play the note):

var audio = new AudioContext();

var volume = 0;
var attack = 1;
var release = 1;

var carrier = audio.createOscillator();
carrier.frequency.value = 440.00;
carrier.type = "sine";
carrier.start();

var carrier_volume = audio.createGain();
carrier_volume.gain.linearRampToValueAtTime(volume, 0);
carrier.connect(carrier_volume);
carrier_volume.connect(audio.destination);

document.addEventListener("keydown", function(e) {
  if(e.keyCode == 81) {
    carrier_volume.gain.linearRampToValueAtTime(1, audio.currentTime + attack);
  }
});

document.addEventListener("keyup", function(e) {
  if(e.keyCode == 81) {
    carrier_volume.gain.linearRampToValueAtTime(0, audio.currentTime + release);
  }
});

(If you are not familiar with the terminology: ‘attack’ is the time it takes to the note to reach it’s peak, 1 second in my example, and ‘release’ is the time it takes to fade away after someone releases the key, also 1 sec on this example).

The problem is this ‘clicking’ sound you can hear before and after the note plays. I did some research:

How can I avoid this ‘clicking’ sound when I stop playing a sound?

http://modernweb.com/2014/03/31/creating-sound-with-the-web-audio-api-and-oscillators/

and found that it would be caused by cutting the sound wave, so I should keep the note playing at 0 dB and raise / lower the volume as needed. However, it only works specifically on Chrome and only if I’m setting volume directly, like so: carrier_volume.gain.value = 1. It does not work, even in Chrome, if I use the linearRampToValueAtTime() function.

Other strange thing happens if I try to set the initial volume to 0 directly. By using carrier_volume.gain.value = 0 on initialization, the first note played will be cut out, but the next notes will play normally.

Does anyone have found a solution to this annoying clicking noises and what is the delay problem when using both gain.value and linearRampToValueAtTime()?

Answer

So, here’s the deal – a linearRampToValueAtTime needs a START time. You’re intending that to be “now” – when the key is pressed – but you need to make it explicit, by setting a current value when the key is pressed. Also, you shouldn’t use linearRamp at the creation – just set the value directly.

If you set the initial volume to 0 directly (via .value) it shouldn’t cut out entirely, but the first ramp won’t have a start point – so it will remain zero until the linearRamp’s time passed, and then it will jump to 1.

Try this:

var audio = new AudioContext();

var volume = 0;
var attack = 1;
var release = 1;

var carrier = audio.createOscillator();
carrier.frequency.value = 440.00;
carrier.type = "sine";
carrier.start();

var carrier_volume = audio.createGain();
carrier_volume.gain.linearRampToValueAtTime(volume, 0);
carrier.connect(carrier_volume);
carrier_volume.connect(audio.destination);

// remember whether we're playing or not; otherwise the keyboard repeat will confuse us
var playing = false;

document.addEventListener("keydown", function(e) {
  if((e.keyCode == 81) && !playing) {
    // first, in case we're overlapping with a release, cancel the release ramp
    carrier_volume.gain.cancelScheduledValues(audio.currentTime);

    // now, make sure to set a "scheduling checkpoint" of the current value
    carrier_volume.gain.setValueAtTime(carrier_volume.gain.value,audio.currentTime);

    // NOW, set the ramp
    carrier_volume.gain.linearRampToValueAtTime(1, audio.currentTime + attack);
    // Note that ideally, we would check the current value from above, and calculate 
    // the length of the attack based on it to keep a constant angle of the attack,
    // rather than a constant time.  (If we're half-way through a release when we 
    // start a new attack, the attack should only take 0.5s since we're already at 0.5.)

    playing = true;
  }
});

document.addEventListener("keyup", function(e) {
  if((e.keyCode == 81) && playing) {
    // first, in case we're overlapping with an attack, cancel the attack ramp
    carrier_volume.gain.cancelScheduledValues(audio.currentTime);

    // now, make sure to set a "scheduling checkpoint" of the current value
    carrier_volume.gain.setValueAtTime(carrier_volume.gain.value,audio.currentTime);

    // NOW, set the ramp
    carrier_volume.gain.linearRampToValueAtTime(0, audio.currentTime + release);

    // Note that ideally, we would check the current value from above, and calculate 
    // the length of the release based on it to keep a constant angle of the release,
    // rather than a constant time.  (If we're half-way through an attack when we 
    // start a new release, the release should only take 0.5s since we're already at 0.5.)
    playing = false;
  }
});