javascriptfirefoxaudioweb-audio-apiaudio-worklet

Simple WebAudioWorklet Generating Choppy Audio


I'm on Firefox 84.0.1, Windows 10, x86_64. I have a very basic WebAudioWorklet synthesiser that maps keys to the frequencies of musical notes. It is generating very choppy audio when a key is held down. This makes me think that there is not enough audio samples being queued for the speaker to play, hence the audio dropping in and out. However, in audio processing terms, I'm performing a very low-intensive task. As a result, I feel like the default Worklet setup should be able to handle this. Here is my code:

syn.js

(async() => {
  let a2_hertz = 110.0;
  let twelfth_root_of_two = Math.pow(2.0, 1.0 / 12.0);
  
  let audio_cxt = new AudioContext();
  await audio_cxt.audioWorklet.addModule("syn-worklet.js", {credentials: "omit"});
 
  let audio_worklet_options = {
    numberOfInputs: 0,
    numberOfOutputs: 1,
    outputChannelCount: [audio_cxt.destination.channelCount]
  };
  let audio_worklet = new AudioWorkletNode(audio_cxt, "synthesiser", audio_worklet_options);
  audio_worklet.connect(audio_cxt.destination);
  
  document.addEventListener("keydown", (evt) => {
    for (let key = 0; key < 12; ++key) {
      if (evt.code == "Key" + "QWERTYUIOPAS"[key]) {
        audio_worklet.port.postMessage(a2_hertz * Math.pow(twelfth_root_of_two, key));
      }
    }
  });
  
  document.addEventListener("keyup", (evt) => {
    audio_worklet.port.postMessage(0.0);
  });
})();

syn-worklet.js

function angular_frequency(hertz) {
  return hertz * 2 * Math.PI;
}

let OSC_TYPES = {"sine": 0, "square": 1, "triangle": 2};
function oscillator(hertz, osc_type) {
  switch (osc_type) {
    case OSC_TYPES.sine: {
      return Math.sin(angular_frequency(hertz) * currentTime);
    } break;
    case OSC_TYPES.square: {
      return Math.sin(angular_frequency(hertz) * currentTime) > 0.0 ? 1.0 : -1.0;
    } break;
    case OSC_TYPES.triangle: {
      return Math.asin(Math.sin(angular_frequency(hertz) * currentTime)) * (2.0 / Math.PI);
    } break;
    default: {
      return 0.0;
    }
  }
}

class Synthesiser extends AudioWorkletProcessor {
  constructor() {
    super();

    this.hertz = 0.0;

    this.port.onmessage = (evt) => {
      this.hertz = evt.data;
    };
  }

  process(inputs, outputs) {
    let channels = outputs[0];
    let num_samples_per_channel = channels[0].length;

    for (let pcm_i = 0; pcm_i < num_samples_per_channel; ++pcm_i) {
      let volume = 0.1;
      let pcm_value = volume * oscillator(this.hertz, OSC_TYPES.sine);
      for (let channel_i = 0; channel_i < channels.length; ++channel_i) {
        channels[channel_i][pcm_i] = pcm_value;
      }
    }

    return true;
  }
}

registerProcessor("synthesiser", Synthesiser);

Solution

  • I think the problem is that currentTime seems to be the only thing which influences the output of your oscillator() function. But currentTime doesn't change during the invocation of the process() function.

    I would recommend using currentFrame instead. It will give you an integer value which represents the currentTime in frames. If you combine that with pcm_i you get the actual index of the sample that you're processing.

    const currentSample = currentFrame + pcm_i;
    const currentSampleInSeconds = (currentFrame + pcm_i) / sampleRate;