NAME

Math::SegmentedEnvelope - create/manage/evaluate segmented (curved) envelope

SYNOPSIS

use Math::SegmentedEnvelope qw(env adsr perc asr concat);

# Create with explicit definition: [levels, durations, curves]
my $e = env(
    [
        [0, 1, 0.8, 0.7, 0],      # N+1 level values
        [0.1, 0.2, 0.4, 0.3],     # N segment durations
        [2, -2, 1, -3]            # N curve values
    ],
    is_morph => 1,
);

# Evaluate at time t
my $value = $e->at(0.5);

# Static evaluator for tight loops (captures state, avoids method dispatch)
my $s = $e->static;
my $value = $s->(0.5);

# Generate 1024-sample lookup table
my @table = $e->table(1024);

# Standard envelope constructors
my $note   = adsr(0.01, 0.1, 0.7, 0.3);
my $kick   = perc(0.001, 0.2);
my $pad    = asr(0.5, 2.0, 1.0);
my $chain  = concat($kick, $pad);

DESCRIPTION

Math::SegmentedEnvelope creates and evaluates segmented envelopes with curved segments. Useful for audio synthesis, animation, or any application requiring smooth interpolation between control points.

Each envelope is defined by N+1 levels (breakpoint values), N durations (time for each segment), and N curves controlling the interpolation shape.

Curve values

The curve parameter controls the shape of interpolation within each segment:

  • Positive curves (e.g. 2, 3) produce ease-in (slow start, fast end)

  • Negative curves (e.g. -2, -3) produce ease-out (fast start, slow end)

  • Curve = 1 or -1 produces linear interpolation

  • Larger absolute values produce more extreme curvature

CONSTRUCTOR

new

my $e = Math::SegmentedEnvelope->new($def, %options);
my $e = Math::SegmentedEnvelope->new(%options);
my $e = Math::SegmentedEnvelope->new();  # random envelope

If no definition is provided, a random envelope is generated (respects Perl's srand for reproducibility).

Options:

def => [$levels, $durations, $curves]

Envelope definition. Can also be passed as the first positional argument.

is_morph => $bool

Apply a smoothing morpher function to the interpolation curve. Default morpher is sin(t * PI/2)^2. See "morpher" and "morpher_formula".

is_hold => $bool

Clamp time to [0, duration]. Without this, time beyond the envelope duration wraps around. With it, the envelope holds at its final value.

is_fold_over => $bool

When time exceeds duration, mirror/fold instead of wrapping. Creates a ping-pong effect.

is_wrap_neg => $bool

Invert the fold direction for negative time values.

morpher => $coderef

Custom morpher function as a Perl code reference. Called with a single argument t in [0,1], must return a value in [0,1]. Has Perl callback overhead.

morpher_formula => $string

A morpher specified as a math expression or predefined name. Compiled to native code via JIT (TCC or x86-64) or evaluated via tinyexpr -- much faster than a Perl callback. Automatically enables is_morph. See "morpher_formula".

border_level => $scalar | [$start, $end]

Default border levels for random envelope generation.

EVALUATION METHODS

at($t)

Evaluate envelope at time $t. Returns the interpolated value. Maintains internal state for optimized sequential access.

static

Returns a Math::SegmentedEnvelope::Static object that captures the current envelope state. Callable as a code reference for performance-critical loops:

my $s = $e->static;
for my $i (0 .. 999) {
    push @samples, $s->($i / 1000 * $e->duration);
}

table($size, $cycles, $from, $to)

Generate a lookup table as a list of $size values.

  • $size - Number of samples (required)

  • $cycles - Number of envelope cycles to fit (default: 1)

  • $from - Start time (default: 0)

  • $to - End time (default: total duration)

my @lfo = $e->table(1024, 4);       # 4 cycles in 1024 samples
my @seg = $e->table(256, 1, 0, 0.5); # first half only

segment_at($t)

Returns the segment index (0-based) at time $t. Useful for triggering events on segment boundaries.

my $seg = $e->segment_at(0.15);  # which segment is active at t=0.15?

area, integral

Returns the definite integral (total area under the envelope curve) as a scalar number. Uses trapezoidal integration across segment breakpoints.

my $total = $e->area;
my $total = $e->integral;

ACCESSOR METHODS

All accessors work as getters when called with no extra arguments, and as setters when called with values.

level($idx, [$value])

Get/set level at index. Supports negative indices (-1 = last level).

levels([@values])

Get/set all levels.

dur($idx, [$value])

Get/set duration at segment index. Supports negative indices.

durs([@values])

Get/set all durations.

curve($idx, [$value])

Get/set curve at segment index. Supports negative indices.

curves([@values])

Get/set all curves.

duration

Returns total duration (sum of all segment durations).

segments

Returns number of segments (N).

def

Returns the envelope definition as [$levels, $durations, $curves].

border_level([$value])

Get/set border levels for random generation. Returns [$start, $end].

is_morph([$bool]), is_hold([$bool]), is_fold_over([$bool]), is_wrap_neg([$bool])

Get/set flag values. See "CONSTRUCTOR" for descriptions.

morpher([$coderef])

Get/set custom Perl morpher callback.

morpher_formula([$formula])

Get/set the morpher formula. Accepts a predefined name or a math expression string with variable t. Pass undef to reset to the default sine morpher.

$env->morpher_formula('smoothstep');
$env->morpher_formula('sin(t * 1.5708) ^ 2');
$env->morpher_formula(undef);  # reset to default

Supported operators: + - * / ^ %. Functions: abs acos asin atan atan2 ceil cos cosh exp floor ln log log10 pow sin sinh sqrt tan tanh.

Setting a formula automatically enables is_morph. The expression is JIT-compiled to native code if possible (via TCC or a built-in x86-64 emitter), falling back to tinyexpr tree interpretation.

morpher_formulas()

Returns the list of predefined morpher names (26 functions):

linear sine smoothstep smootherstep welch tanh
quad_in quad_out quad_inout
cubic_in cubic_out cubic_inout
circ_in circ_out circ_inout
exp_in exp_out
back_in back_out back_inout
elastic_in elastic_out elastic_inout
bounce_in bounce_out bounce_inout

morpher_jit_backend

Returns the morpher backend: "tcc" (TCC JIT), "x86" (hand-rolled JIT), "builtin" (predefined C function), or "none" (default/tinyexpr).

UTILITY METHODS

normalize_duration

Normalize durations so they sum to 1.0. Returns $self.

clean

Reset internal cached state. Call this after modifying segment data via setters if you plan to evaluate at non-sequential time values.

TRANSFORMATION METHODS

All transformations return a new envelope, leaving the original unchanged.

scale($factor)

Multiply all levels by $factor (default: 1.0).

my $louder  = $e->scale(2.0);
my $quieter = $e->scale(0.5);

offset($value)

Add $value to all levels (default: 0).

my $shifted = $e->offset(0.5);  # shift entire envelope up by 0.5

invert

Flip all levels: 1 - level. Useful for inverting an envelope shape.

my $inv = $e->invert;

stretch($factor)

Scale all durations by $factor (default: 1.0). Levels and curves are preserved.

my $slow = $e->stretch(2.0);   # twice as long
my $fast = $e->stretch(0.5);   # half duration

with_duration($duration)

Scale durations so the envelope has the specified total duration. Returns a new envelope.

my $two_sec = $e->with_duration(2.0);

clone, copy

Create a fast deep copy of the envelope in C.

my $dup = $e->clone;

reverse

Reverse the envelope direction. Durations are reversed and curve signs are flipped.

delay($time)

Prepend a hold at the initial level for $time seconds.

my $delayed = $e->delay(0.5);

add($other, %opts)

Add two envelopes sample-by-sample. Returns a resampled envelope.

my $sum = $e1->add($e2, segments => 32);

multiply($other, %opts)

Multiply two envelopes sample-by-sample (ring modulation, AM).

my $am = $carrier->multiply($modulator, segments => 64);

normalize($lo, $hi)

Scale levels to fit within [$lo, $hi] (default [0, 1]). Uses min_value and max_value to determine current range.

my $n = $e->normalize;           # [0, 1]
my $n = $e->normalize(-1, 1);    # bipolar

loop($n)

Repeat the envelope $n times end-to-end. Uses concat internally.

my $lfo = $cycle->loop(16);      # 16 repetitions

map_levels($coderef)

Apply a function to every level. Durations and curves are preserved.

my $gamma = $e->map_levels(sub { $_[0] ** 2.2 });
my $clamp = $e->map_levels(sub { $_[0] > 0.5 ? 0.5 : $_[0] });

quantize($steps)

Snap all levels to $steps discrete values. Returns a new envelope. Implemented in XS.

my $lofi = $e->quantize(4);   # levels snapped to 0, 0.25, 0.5, 0.75, 1.0

trim($from, $to, %opts)

Extract a time slice as a new envelope by resampling. Implemented in XS.

my $attack_only = $e->trim(0, 0.2);
my $hires = $e->trim(0.1, 0.5, segments => 64);

lerp($other, $mix)

Interpolate between two envelopes with the same segment count. Unlike blend, this interpolates levels, durations, and curves directly without resampling -- much cheaper. Implemented in XS.

my $mid = $bright->lerp($soft, 0.5);

Croaks if segment counts differ (use blend for that case).

resample($n)

Re-approximate the envelope with exactly $n segments by sampling. Useful for simplifying high-segment-count results from blend or spline. Implemented in XS.

my $simple = $complex->resample(8);

clamp($lo, $hi)

Clamp all levels to [$lo, $hi]. Implemented in XS.

my $safe = $e->clamp(0, 1);

smooth($passes)

Apply moving-average smoothing to levels. Endpoints are preserved. $passes defaults to 1; higher values produce smoother results. Implemented in XS.

my $soft = $noisy->smooth(3);

derivative

Returns a new envelope of the rate of change (slope) at each segment boundary. Implemented in XS.

my $slope = $e->derivative;

integrate

Returns the cumulative integral (area under the curve) via trapezoidal rule on the breakpoint levels. Starts at 0. Implemented in XS.

Note: the trapezoidal approximation is exact for linear segments (curve=1) but approximate for curved segments. For better accuracy on curved envelopes, resample to more segments first: $e->resample(64)->integrate.

my $area = $e->integrate;

from_samples(\@values, $duration)

Create an envelope from raw sample data (the inverse of table). Each value becomes a level; segments are evenly spaced across $duration. Implemented in XS. Works as class method or exported function.

my $e = Math::SegmentedEnvelope->from_samples([0, 0.5, 1, 0.5, 0], 1.0);

to_svg(%opts)

Returns an SVG string of the envelope shape.

Options: width (400), height (150), padding (10), stroke (color), fill (color), samples (width - 2*padding).

my $svg = $e->to_svg(width => 600, height => 200);

to_supercollider(%opts)

Returns a SuperCollider Env() definition string. Option: name (variable name, default: "env").

print $e->to_supercollider(name => 'ampEnv');
# var ampEnv = Env([0, 1, 0.7, 0], [0.1, 0.3, 0.5], [2, -2, 1]);

to_csound

Returns a Csound linseg statement string.

print $e->to_csound;
# linseg 0, 0.1, 1, 0.3, 0.7, 0.5, 0

to_glsl(%opts)

Returns a GLSL function that evaluates the envelope on the GPU. Samples the envelope into a float array with linear interpolation. Options: name (function name, default "envelope"), samples (default 64).

print $e->to_glsl(name => 'ampEnv', samples => 32);

as_pdl

Returns the breakpoint levels as a PDL piddle (N+1 elements). No sampling -- direct binary copy from the internal C array. Requires PDL (loaded on demand).

my $levels = $e->as_pdl;   # fast, no resampling

to_pdl($samples)

Returns $samples (default 1024) evenly-sampled envelope values as a PDL piddle. Uses _raw_table internally to avoid Perl-level array intermediary.

my $sig = $e->to_pdl(4096);

from_pdl($piddle, $duration)

Create an envelope from a PDL piddle. Class method.

my $e = Math::SegmentedEnvelope->from_pdl($piddle, 2.0);

blend($other, $mix, %opts)

Blend with another envelope by resampling both.

my $blended = $e1->blend($e2, 0.3);  # 70% e1, 30% e2

Options: segments (default 32), duration (default: self's duration).

concat(@envelopes)

Concatenate envelopes end-to-end. Works as function or class method. The result inherits configuration options (such as is_hold, is_fold_over, etc.) from the first envelope.

my $chain = concat($e1, $e2, $e3);

min_value($samples)

Returns the minimum value of the envelope, estimated by sampling $samples points (default 1024).

max_value($samples)

Returns the maximum value, estimated by sampling.

to_hash

Serialize the envelope to a hash reference containing def, flags, and morpher_formula. Suitable for JSON encoding.

my $h = $e->to_hash;
# { def => [...], is_morph => 1, morpher_formula => 'smoothstep', ... }

from_hash

Reconstruct an envelope from a hash reference (as produced by "to_hash"):

my $e = Math::SegmentedEnvelope->from_hash($h);

SERIALIZATION & STORABLE

Math::SegmentedEnvelope supports Storable serialization natively via STORABLE_freeze and STORABLE_thaw hooks:

use Storable qw(freeze thaw dclone);

my $frozen = freeze($e);
my $thawed = thaw($frozen);
my $cloned = dclone($e);

For JSON or custom storage, use "to_hash" and "from_hash".

STORABLE_freeze

Internal hook for Storable serialization. Returns a frozen scalar representation of the envelope.

STORABLE_thaw

Internal hook for Storable deserialization. Restores envelope state from frozen data.

EXPORTED FUNCTIONS

env(...)

Shortcut for Math::SegmentedEnvelope->new(...).

adsr($attack, $decay, $sustain, $release, %opts)

ADSR envelope. $sustain is a fraction of peak (0-1). Options: peak, sustain_time, attack_curve, decay_curve, release_curve, plus standard envelope options.

perc($attack, $decay, %opts)

Percussive envelope (attack to peak, decay to zero). Options: peak, attack_curve, decay_curve.

asr($attack, $sustain_time, $release, %opts)

Attack-Sustain-Release envelope. Options: peak, attack_curve, release_curve.

concat(@envelopes)

Concatenate envelopes. See "TRANSFORMATION METHODS".

morpher_formulas()

Returns predefined morpher names. See "ACCESSOR METHODS".

spline(\@times, \@values, %opts)

Create a smooth Catmull-Rom spline envelope through control points. Each control point is a (time, value) pair. The spline passes exactly through all control points. Implemented in XS.

use Math::SegmentedEnvelope 'spline';

my $e = spline(
    [0, 0.3, 0.7, 1.0],    # times
    [0, 1.0, 0.2, 0.8],    # values
    resolution => 16,        # segments per span (default: 8)
    tension    => 0.0,       # 0=Catmull-Rom, 1=linear (default: 0)
);

Also works as a class method: Math::SegmentedEnvelope->spline(...). Accepts standard envelope options (is_morph, morpher_formula, etc.).

EXAMPLES

Audio note envelope

use Math::SegmentedEnvelope qw(adsr);

my $env = adsr(0.01, 0.1, 0.7, 0.3, is_morph => 1);
my $s = $env->static;

# Generate amplitude envelope for 44100 Hz audio
my $sr = 44100;
my $dur = $env->duration;
for my $i (0 .. int($dur * $sr) - 1) {
    my $amp = $s->($i / $sr);
    # ... apply $amp to audio sample ...
}

LFO wavetable

use Math::SegmentedEnvelope 'env';

my $lfo = env(
    [[0, 1, 0, -1, 0], [0.25, 0.25, 0.25, 0.25], [2, -2, 2, -2]],
    is_morph => 1,
);
my @wavetable = $lfo->table(1024, 1);  # one cycle, 1024 samples

Ping-pong envelope

use Math::SegmentedEnvelope qw(perc concat);

my $up   = perc(0.01, 0.5);
my $down = $up->reverse;
my $pingpong = concat($up, $down);

Custom morpher formula

my $e = env(
    [[0, 1, 0], [0.5, 0.5], [1, 1]],
    is_morph => 1,
    morpher_formula => 't * t * (3 - 2 * t)',  # smoothstep
);
printf "Backend: %s\n", $e->morpher_jit_backend;  # "tcc", "x86", or "builtin"

Math::SegmentedEnvelope::Static

Returned by "static". Overloads &{} so it can be called as a code reference.

my $s = $e->static;
my $val = $s->(0.5);          # via overload
my $val = $s->call(0.5);      # explicit method

OPERATOR OVERLOADING

Math::SegmentedEnvelope overloads the following operators:

  • &{} - Callable as a code reference:

    my $val = $e->(0.5);   # equivalent to $e->at(0.5)
  • + - Offset or addition:

    my $shifted = $e + 0.5;   # equivalent to $e->offset(0.5)
    my $sum     = $e1 + $e2;  # equivalent to $e1->add($e2)
  • - - Subtraction or negation:

    my $shifted = $e - 0.2;   # equivalent to $e->offset(-0.2)
    my $diff    = $e1 - $e2;  # equivalent to $e1->add($e2->scale(-1))
    my $inv     = 1 - $e;     # invert envelope shape
  • * - Scaling or multiplication:

    my $louder  = $e * 1.5;   # equivalent to $e->scale(1.5)
    my $ringmod = $e1 * $e2;  # equivalent to $e1->multiply($e2)
  • neg - Invert sign:

    my $neg = -$e;            # equivalent to $e->scale(-1)
  • "" - Stringification:

    print "$e\n";  # Math::SegmentedEnvelope(segments=4, duration=1.2s)

AUTHOR

Yegor Korablev <egor@cpan.org>

LICENSE

This is free software; you can redistribute it and/or modify it under the same terms as Perl itself.