npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@synthlet/adsr

v0.2.0

Published

Adsr generator audio worklet

Readme

@synthlet/adsr

Attack-Decay-Sustain-Release envelope generator, as an AudioWorklet

Part of Synthlet

Web Audio has no envelope generator. This is one, in two shapes: AdsrAmp is an amplifier — one input, multiplied by the envelope — and AdsrEnv is a modulator with no input, to connect at any AudioParam.

Install

npm i @synthlet/adsr

Or npm i synthlet for every module, from which the same names are exported.

AdsrAmp

The envelope as a VCA. Register the worklet on the context first, then build the graph the ordinary way:

import { registerAdsrWorklet, AdsrAmp } from "@synthlet/adsr";

const ac = new AudioContext();
await registerAdsrWorklet(ac);

const amp = AdsrAmp(ac, {
  attack: 0.01,
  decay: 0.1,
  sustain: 0.7,
  release: 0.3,
});

const osc = new OscillatorNode(ac, { frequency: 440 });
osc.start();
osc.connect(amp).connect(ac.destination);

amp.gate.setValueAtTime(1, ac.currentTime); // note on
amp.gate.setValueAtTime(0, ac.currentTime + 0.5); // note off

// ...when you're done with it
amp.dispose();

Registration is asynchronous and has to happen before you create anything: an AudioWorkletProcessor can't fetch its own code, so it must be installed on the context first. Everything after that is synchronous.

Modules are functions, not classes, so there's no new. They start themselves, so there's no start().

An AdsrAmp whose input is disconnected, or whose source has stopped, reads as silence rather than failing. You can leave one in the graph between notes and reconnect a source later; it keeps running either way.

AdsrEnv

The same envelope with no input, to modulate something else. gain and offset scale the 0…1 envelope into whatever units the destination wants — here a filter sweep from 200 Hz to 2200 Hz:

import { registerAdsrWorklet, AdsrEnv } from "@synthlet/adsr";

const ac = new AudioContext();
await registerAdsrWorklet(ac);

const filter = new BiquadFilterNode(ac, { type: "lowpass" });

const env = AdsrEnv(ac, {
  attack: 0.05,
  decay: 0.4,
  sustain: 0.2,
  release: 0.5,
  gain: 2000,
  offset: 200,
});

env.connect(filter.frequency);
env.gate.setValueAtTime(1, ac.currentTime);

Both factories share one processor, so a single registerAdsrWorklet covers them both.

Parameters

Every parameter is an AudioParam, so it can be set, scheduled, or driven by another node. Times are in seconds.

| Param | Default | Min | Max | Meaning | | --------- | ------- | ------ | ----- | --------------------------------------------------- | | gate | 0 | 0 | 1 | Opens the envelope while positive, releases it at 0 | | attack | 0.01 | 0 | 10 | Time to reach full level | | decay | 0.1 | 0 | 10 | Time from full level down to sustain | | sustain | 0.5 | 0 | 1 | Level held while the gate stays open | | release | 0.3 | 0 | 10 | Time from the current level down to zero | | offset | 0 | -20000 | 20000 | Added to the output | | gain | 1 | -20000 | 20000 | Multiplies the output |

AdsrEnv outputs envelope × gain + offset; AdsrAmp outputs input × envelope × gain + offset. The same list is available at runtime as AdsrEnv.descriptors / AdsrAmp.descriptors, if you're generating UI from it.

A parameter accepts a node wherever it accepts a number, which is how you modulate one — here with an Lfo, from the umbrella package:

import { AdsrEnv, Lfo, LfoType } from "synthlet";

// A slow sine between 0.25 and 0.75, driving the sustain level
const wobble = Lfo(ac, {
  type: LfoType.Sine,
  frequency: 0.3,
  gain: 0.25,
  offset: 0.5,
});
const env = AdsrEnv(ac, { sustain: wobble });

Parameters are also reachable on the node:

const env = AdsrEnv(ac, { attack: 0.01 });
env.attack.value = 0.2;

The gate

The envelope is open while gate is positive and releases when it returns to zero. Any positive value opens it — 1 is the convention, not a requirement — so an attenuated or scaled gate line still works.

gate is read once per render block (~2.9 ms at 44.1 kHz), so a note on and a note off in the same tick is invisible to the worklet:

// Wrong: nothing is left for the processor to see
amp.gate.value = 1;
amp.gate.value = 0;

// Right: schedule the edges
amp.gate.setValueAtTime(1, ac.currentTime);
amp.gate.setValueAtTime(0, ac.currentTime + 0.5);

Don't smooth a gate line. Use setValueAtTime or linearRampToValueAtTime; setTargetAtTime approaches zero without ever arriving, so the envelope never releases. The envelope is the smoother — that's what it's for.

Retrigger is legato

Opening the gate again during the release phase resumes the attack from the current level, rather than restarting from zero. A quick re-press continues from where the release got to, so there's no click and no dropout. This is Redmon's behaviour, and it is the only mode: there is no retrigger parameter.

If you want a hard restart, close the gate long enough for the release to reach zero before reopening it.

Sustain changes apply instantly

While the envelope is sustaining, the output tracks sustain sample by sample. Automating sustain mid-note steps to the new value rather than gliding to it, which can click at audio-rate amplitudes. Change it between notes, or ramp the gain instead.

Offset is silence

The envelope outputs offset when the gate is closed, so a non-zero offset is a permanent DC floor under an AdsrAmp — the amplifier never goes fully quiet. Leave offset at 0 unless you specifically want that floor, which is usually only when driving a parameter that shouldn't reach zero:

// A filter that sweeps 200 Hz -> 2200 Hz and rests at 200 Hz, not at 0 Hz
const env = AdsrEnv(ac, { gain: 2000, offset: 200 });

Credits

Based on Nigel Redmon's ADSR code (earlevel.com), with two TCO constants from Will Pirkle's SynthLab (Tritone Systems).

See the repository's THIRD-PARTY-LICENSES.md.

License

MIT © danigb