1.0.1 • Published 5 years ago

simple-audio-worklet v1.0.1

Weekly downloads
1
License
ISC
Repository
-
Last release
5 years ago

Simple Audio Worklet

AudioWorklets handle audio in blocks of 128 frames at a time, provide parameters that can be arrays of either one or 128 values, and make setup that depends on parameters difficult.

This package abstracts the complexity and focuses on the actual processing, one frame at a time. It works with generator functions, classes, and pure functions, and produces efficient code that does not even allocate any memory after initialization.

If you are not already familiar with using Audio Worklets, see this article.

Usage

Whether a generator function, class, or pure function, use this package like this:

let processor = fromX(yourImplementation, options);

where options looks like the following:

{
  // If true, stops running the worklet if there are no input nodes connected.
  processOnly?: boolean,
  // An array of descriptors describing the parameters this worklet
  // takes. These correspond to the parameters sent to the implementation.
  parameterDescriptors?: AudioParamDescriptor[],
  // Handles incoming messages from the paired AudioWorkletNode.
  onmessage?: (this: MessagePort, ev: MessageEvent) => any,
  // If a non-empty string, calls registerProcessor(...) on the generated class.
  registerAs?: string
}

The implementation will be called each frame with args as follows:

{
  // An array containing the sample value for each input channel.
  // Empty if not connected.
  input: number[],
  // An object of the parameters declared in parameterDescriptors
  // and their corresponding values.
  readonly parameters: {
    [param: string]: number
  },
  // A port usable for communication with the paired AudioWorkletNode.
  readonly port: MessagePort,
  // An "environment" containing useful values. These values can
  // change but usually won't.
  readonly env: {
    outputChannelCount: number,
    sampleRate: number
  }
}

and expected to return either a number or a Float32Array.

A returned Float32Array will map one-to-one with the output frame channels, if it is long enough. If not, the first value of the array will be treated as a returned number.

A returned number will be treated as a mono frame, and will be copied to all output channels.

Generator function

Generator functions work as straightforward representations of asynchronous processing, which makes them well suited to audio processing applications. Most cases will involve an infinite loop with all processing done inside, and optional setup done beforehand.

For many cases, a generator function is the most readable and writable way to express a DSP algorithm.

import { fromGenerator } from 'simple-audio-worklet';

function* addNoise({parameters, input, port}) {
  // setup would go here
  for (;;) {
    let {gain} = parameters; // or just use parameters.gain
    for (let channel = 0; channel < input.length; ++channel) {
      let noise = (Math.random() * 2 - 1) * gain;
      input[channel] += noise; // Safe and faster to modify input in place
    }
    yield input;
  }
}
let processor = fromGenerator(addNoise, {
  processOnly: true,
  parameterDescriptors: [{
    name: 'gain',
    minValue: 0,
    maxValue: 1,
    defaultValue: 0.5
  }]
});

Note: Because of the nature of generators, there is a one-frame delay on the output, which may not be suitable for some cases. The very first frame is zero-filled.

Note: The identity of args, args.parameters, args.input, and args.port is guaranteed not to change. Therefore, it is okay to use

yield x;

whereas otherwise one would have to use

({parameters, input} = yield x);

However, the values for the individual parameters must be accessed on the parameters object or updated every yield, or else the values will become outdated. For example,

function* process({parameters, input, port}) {
  let {param} = parameters; // BAD, will go stale
  for (;;) {
    let {param} = parameters; // OK
    ... parameters.param ...; // OK
    // compute something as result
    yield result;
  }
}

Class

The class version looks similar to the standards-compliant processor, but with frame-based processing as opposed to block-based processing. It must define a next() method instead of a process() method, and the parameters differ. It may be more familiar to object oriented programmers than the generator function, has no frame delay, and can use state.

import { fromClass } from 'simple-audio-worklet';

class AddNoise {
  constructor({parameters, input, port}) {
    // Initialize some instance variables,
    // same arguments as on first call to next()
  }

  next({parameters, input, port}) {
    for (let channel = 1; channel < input.length; ++channel) {
      let noise = (Math.random() * 2 - 1) * parameters.gain;
      input[channel] += noise;
    }
    return input;
  }
}

let processor = fromClass(AddNoise, {
  processOnly: true,
  parameterDescriptors: [{
    name: 'gain',
    minValue: 0,
    maxValue: 1,
    defaultValue: 0.5
  }]
});

Pure function

The pure function implementation is best when no state is necessary. Every output frame should be based solely on the frame input and the parameters, not on previous frames.

import { fromPure } from 'simple-audio-worklet';

function addNoise({parameters, input, port}) {
  for (let channel = 1; channel < input.length; ++channel) {
    let noise = (Math.random() * 2 - 1) * parameters.gain;
    input[channel] += noise;
  }
  return input;
}

let processor = fromPure(addNoise, {
  processOnly: true,
  parameterDescriptors: [{
    name: 'gain',
    minValue: 0,
    maxValue: 1,
    defaultValue: 0.5
  }]
});

It is important to keep this function pure in the sense that it does not store information by capturing variables and writing to them; alternatively, we can say the function must be memoryless. This limitation is in place because multiple instances of the processor would be capturing the same variables, leading to unexpected results. Capturing a constant value is fine.

Installation

$ npm install simple-audio-worklet

Browser Module

In Chromium and potentially other browsers, worklets can now use standard ES6 module loading. This means this package can be included into browser code with something like

<script
   src="simple-audio-worklet.js"
   type="module">
</script>

where simple-audio-worklet.js has been copied from /node_modules/simple-audio-worklet/lib/index.js and then running

let context = new AudioContext();
await context.audioWorklet.addModule("my-audio-worklet.js");

with my-audio-worklet.js containing

import { fromGenerator, fromClass, fromPure } from 'simple-audio-worklet';

Webpack

Packing files in worklet global scopes with packages like worklet-loader can work but is still relatively immature at time of writing.