Envelope

The notion of an envelope is borrowed from sound synthesis. They are useful for modulating a value after an initial trigger, with simple means for describing the shape of the modulation.

Envelopes have some similarity with easing functions, as they describe a shape over time.

Anatomy of an envelope

The envelope consists of a series of stages, typically attack, decay, sustain and release.

  • All stages have an associated level or amplitude. Attack's level is also known as the initial level, and decay's level is also known as the peak level.
  • All stages except sustain have a duration, how long they run for in milliseconds.

When a trigger happens (eg. a synth key is pressed), the attack stage runs for its specified duration, after which the decay stage runs. The sustain stage runs for as long as the trigger is held. At any point when the key is released, the release stage runs.

As a stage progresses, it is essentially interpolating from its start to end point. Internally, each stage is modelled as running from 0 to 1, but this is scaled according to the levels you define.

Envelopes can also loop through the attack, decay and release stages whilst being triggered. In this case, the sustain stage is skipped.

In ixfx, interpolation for each stage happens using a curve, allowing for more expressive progressions with the bend parameter.

undefined

Playground

The playground uses the settings from the envelope editor above. You can trigger the envelope, which will then run through its stages. Use Trigger & Hold if you want to have the envelope hold at the sustain stage. Release allows a held envelope to continue on to the release stage.

undefined

Usage

Docs: Adsr Type

Initialise an envelope with a few timing settings:

import { Envelopes } from "https://unpkg.com/ixfx/dist/modulation.js"

// It's a good idea to use the defaultAdsrOpts(),
// and then override what you want.
const opts = {
  ...Envelopes.defaultAdsrOpts(),
  attackDuration: 1000,
  decayDuration: 200,
  sustainDuration: 100
};
const env = Envelopes.adsr(opts);

In basic usage, you first trigger the envelope, and then read its value over time, probably from some kind of loop.

env.trigger();
setInterval(() => {
  console.log(env.value); // 0..1
});

You can 'trigger-and-hold', making the envelope stay at the sustain stage until 'release' is called:

// Trigger and hold at 'sustain' stage
env.trigger(true);
// ...at some point later, allow it to continue to 'release' stage.
env.release();

Fetching the value property gives you the value of the envelope at that point in time. You can get additional data with compute:

// Gets:
// name of current stage (as a string), 
// scaled value (same as calling .value)
// and raw value (0 -> 1 progress *within* a stage)
const [stage, scaled, raw] = env.compute();

You can see an envelope in action on fn-vis.

Other functions:

// Reset envelope
env.reset();

// True if envelope is finished
env.isDone;

Envelopes have events:

// Envelope has changed stage
env.addEventListener(`change`, ev => {
  console.log(`Old: ${evt.oldState} new: ${ev.newState}`);
})

// Envelope has finished
env.addEventListener(`complete`, () => {
  console.log(`Done.`);
})

Envelope options

Envelope options are documented here and the timing options here.

There are three 'bend' options for setting a stage curve, attackBend, decayBend and releaseBend. Bend values run from -1 to 1. A value of 0 means there is no bend (ie. straight line), -1 pulls curve down, and 1 pushes it outward.

eg:

const opts = {
  ...defaultAdsrOpts(),
  attackBend: -1,
  decayBend: 0.5,
  releaseBend: 0
}

Levels can be set via initialLevel, peakLevel, releaseLevel and sustainLevel. These are presumed to be 0 to 1, inclusive. Typically the initial level is 0, the peak 1 and release 0 (these are the defaults).

eg:

const opts = {
  ...defaultAdsrOpts(),
  initialLevel: 0,
  peakLevel: 1,
  releaseLevel: 0
}

retrigger means a retriggered envelope continues its value from what it is at the point of retrigger. By default, as retrigger is false, envelope always start 0 (or whatever initialLevel is set to).

const opts = {
  ...defaultAdrsOpts(),
  retrigger: true
}

Envelopes in action

Here is a pattern to request the envelope value over time. After setting up the envelope, we use a loop to read the value at a given period.

import { Envelopes } from "https://unpkg.com/ixfx/dist/modulation.js"
import { continuously } from "https://unpkg.com/ixfx/dist/flow.js"

// Initialise
const settings = Object.freeze({
  env: Envelopes.adsr({
    ...Envelopes.defaultAdsrOpts()
  },
  sampleRateMs: 100
});

let state = {
  envSampler
};

// Run a loop, reading from envelope until done
state.envSampler = continuously(() => {
  const { env } = settings;
  // If envelope is done, stop looping
  if (env.isDone) return false; 

  // Read value from envelope, do something with it...
  const v = env.value;
}, settings.sampleRateMs);

// Trigger envelope and start reading
settings.env.trigger();
state.envSampler.start();

Or perhaps you want to start an envelope when an event happens, such as a button clicked. We can introduce a retrigger() function that cancels the sampler, triggers the envelope and starts the sampler again

const retrigger = () => {
  const { env } = settings;
  const { envSampler } = state;

  envSampler.cancel();
  env.trigger();
  envSampler.start();
};

document.getElementById(`someButton`).addEventListener(`click`, retrigger);

In the demo below, pointerdown or keydown events triggers and holds the envelope. On the left side you see a typical binary on/off response, on the right you see a gradual effect of the envelope.

Releasing the pointer or key calls the envelope's release function.

This envelope has retrigger disabled, so pressing again while it's decaying will continue the envelope at that level, rather than resetting to zero (default behaviour).