Lab

envelope

The envelope's controller from @audio-ui/core: breakpoints held or controlled, moved, added, removed and bent by pointer and keyboard, and the geometry that draws a bent segment.

pnpm add @audio-ui/core
import { createEnvelopeController, CURVE_EPSILON, CURVE_HIT_TOLERANCE, CURVE_RATE, CURVE_SAMPLES, /* … */ } from "@audio-ui/core/envelope";
Adapters@audio-ui/react/envelope, @audio-ui/elements/envelope
Used byarrangement, dawproject, use-arrangement
ExportKindWhat it is
createEnvelopeControllerfunctionAn envelope's behavior with no view attached: its points, held or controlled, every way of moving, adding, removing and bending them, and what it looks like as a snapshot.
EnvelopeChangeEventDetailstypeWhat onValueChange receives with each change: the index of the point or segment, the reason, the event behind it, and cancel() to refuse the change.
EnvelopeChangeEventReasontypeWhy the points changed: "drag", "keyboard", "add", "remove" or "curve" (a bend).
EnvelopeCommitEventDetailstypeWhat onValueCommitted receives once a gesture ends: the index of the point or segment, the reason and the event behind it.
EnvelopeCommitEventReasontypeWhy the points were committed: the same reasons as a change.
EnvelopeControllerinterfaceWhat createEnvelopeController returns: the snapshot to draw from, and a call for each gesture and key that moves, adds, removes, bends or straightens.
EnvelopeOptionsinterfaceThe options of createEnvelopeController: the points, held or controlled, how few and how many there may be, the grid they snap to, the keyboard step, the formatting and the callbacks.
EnvelopeSnaptypeA grid step in normalized units: one for both axes, or one for each (0 leaves that axis free).
EnvelopeSnapshotinterfaceWhat a view draws an envelope from.
BreakpointtypeA point of the ramp, and how the segment leaving it bends: curve from -1 to 1, 0 or unset for a straight line.
CURVE_EPSILONconstA curve this close to flat is taken as straight, and dropped.
CURVE_HIT_TOLERANCEconstHow far from a curve, as a share of the graph's height, a press still counts as on it.
CURVE_RATEconstHow far a segment can bend: the rate of its exponential at the ends of -1…1, which puts the middle of the segment at 0.4% of its rise (or 99.6%).
CURVE_SAMPLESconstStraight lines between samples of a bent segment: where its drawing starts, before it adds vertices where the bend is tight.
CURVE_STEP_MULTIPLIERconstKeyboard step for a segment's curve, as a multiple of step.
clampToNeighborsfunctionClamp a candidate point's x between its neighbors in base (so points can never cross or reorder) and y to [0,1].
curveForMidpointfunctionThe curve that puts a segment's midpoint at height y, or null when the segment is flat and has nothing to bend.
curveKeyDeltafunctionHow far a key bends a segment, or null for a key that does not: up lifts the handle, so a rising segment bends one way and a falling one the other.
curveSamplesfunctionWhere along a bent segment to put the vertices of its drawing: evenly in length, and evenly in height, so a steep stretch is sampled as finely as a flat one and does not turn into a corner.
insertPointfunctionpoint added to base.
LARGE_STEP_MULTIPLIERconstShift + arrows move this many steps at once.
levelAtfunctionWhere a bent segment is at a share t of its length, as a level.
polylinePointsfunctionPolyline coordinates: flat lead-in from x=0 to the first point, the points themselves with each bent segment sampled between them, flat lead-out from the last point to x=1.
roundCoordfunctionOne decimal place is plenty of precision for a 0-100 SVG coordinate, and keeps the points string short during drag.
samePointsfunctionWhether two lists hold the same points, curves included, in the same order.
sanitizePointsfunctionThe points kept on the graph: each within 0 and 1, a curve within -1 and 1 (dropped when it is flat), in order along x.
segmentNearfunctionThe segment that has the point (x, y) on it or close to it, or null: one that is not flat, since a flat segment has nothing to bend.
segmentShapefunctionWhere along a bent segment a share t of its length lands, as a share of its rise: the exponential of an analog envelope stage, (e^(k·t) - 1) / (e^k - 1) with k the curve times CURVE_RATE.
snapTofunctionRounds a normalized coordinate to the nearest multiple of step; a step of 0 leaves it.
sortPointsfunctionThe points in order along x.

Usage

An envelope is a list of breakpoints, each a point from 0 to 1 on both axes, with a curve from -1 to 1 for how the segment leaving it bends (0, or none, for a straight line). The controller holds that list and every gesture on it, and follows the contract every controller shares:

import { createEnvelopeController } from "@audio-ui/core/envelope";

const envelope = createEnvelopeController({
  defaultValue: [
    { x: 0, y: 0 },
    { x: 0.1, y: 1, curve: -0.5 },
    { x: 0.4, y: 0.6 },
    { x: 1, y: 0 },
  ],
  minPoints: 2,
  snap: 0.05,
  onValueCommitted: (points) => save(points),
});
const unsubscribe = envelope.subscribe(() => draw(envelope.getSnapshot()));
OptionMeaning
value, defaultValueThe points, controlled, or the uncontrolled start (an ADSR-like shape unless given)
minPoints, maxPointsHow few (2 by default) and how many points it may have
snapA grid step for both axes, or { x, y } for each, 0 leaving an axis free; Cmd or Ctrl held skips it
stepWhat an arrow key moves a point by, 0.01 by default
format, localeHow formatPoint writes a point for a screen reader
disabledTakes every input away

The input

Each gesture has its own call, given the element it happens on and the graph's element, which gives the coordinates:

  • pressPoint(index, event, handle, graph) drags a point, or removes it with Alt held; a point stays between its neighbours, so points never cross.
  • pressSegment(index, event, handle, graph) drags a segment's handle to bend it, and pressBend(event, graph), an Alt + press on the graph, bends the segment under the pointer.
  • addPointAt({ x, y }, event) adds a point and returns its index, or null at maxPoints; removePoint(index, event) takes one away, down to minPoints.
  • straightenSegment(index, event) and straightenAt({ x, y }, event) make a bent segment straight again.
  • keyPoint(index, event) moves a focused point with the arrows, Shift for a larger step, and removes it with Delete or Backspace; keySegment(index, event) bends a focused segment with Up and Down and straightens it with Delete. Each returns true when it handled the key.

The snapshot holds the points and what is being dragged (draggingIndex, draggingSegmentIndex), with blockedIndex and isAddBlocked for a moment after a removal or an addition was refused, for a view to show it. dispose() releases that feedback's timers when the view goes.

Geometry

The rest of the module draws the envelope: polylinePoints is the polyline through the points with each bent segment sampled, levelAt the level along a bent segment, curveForMidpoint the curve that puts a segment's middle at a height, and segmentNear the segment under a point. sortPoints, sanitizePoints, clampToNeighbors and insertPoint keep a list valid.

The Envelope draws this controller in React.

Built by Ouest Labs, the source code is available on GitHub.
RSS