Env

Env

Overview

This document describes env, a trigger based envelope generator with adjustable attack, hold, and release times. This envelope is produced by feeding a gate signal into a smoothing filter. The output returns an exponetial envelope with very nice-sounding curves: a convex rise, and a concave fall.

This sort of envelope is very ideal for percussive sounds and for mimicking analogue synthesizers. The drawbacks mainly have to do with precision. Attack and release times are specified as time constants, which are a little bit less intuitive than using seconds. The signal generated is in range 0 to 1, but it will never reach 1 exactly.

Tangled Files

env.c and env.h, respectively.

<<env.h>>=
#ifndef SK_ENV_H
#define SK_ENV_H
#ifndef SKFLT
#define SKFLT float
#endif

<<typedefs>>
<<funcdefs>>

#ifdef SK_ENV_PRIV
<<structs>>
#endif

#endif

<<env.c>>=
#include <math.h>
#define SK_ENV_PRIV
#include "env.h"
<<macros>>
<<funcs>>

Initialization

Initialize the envelope with sk_env_init.

<<funcdefs>>=
void sk_env_init(sk_env *env, int sr);

<<funcs>>=
void sk_env_init(sk_env *env, int sr)
{
    env->sr = sr;
    <<init>>
}

Struct Components

Struct

<<typedefs>>=
typedef struct sk_env sk_env;

<<structs>>=
struct sk_env {
    int sr;
    <<sk_env>>
};

Timer

A timer is used to produce a gate signal of a particular duration. This kicks on during the hold portion of the signal.

This timer is stored in a normalized range, and is updated by a normalized floating-point incrementor value.

<<sk_env>>=
float timer;
float inc;

<<init>>=
env->timer = 0;
env->inc = 0;

Filter

The filter used is a pretty typical one-pole lowpass filter, but with two parameters for the release and attack portions of the envelope.

<<sk_env>>=
SKFLT atk_env;
SKFLT rel_env;

<<init>>=
env->atk_env = 0;
env->rel_env = 0;

State Management

The envelope has three main states: attack, hold, and release. There is also a zero state to inidicate that envelope is not playing anything.

<<sk_env>>=
int mode;

<<macros>>=
enum {
    MODE_ZERO,
    MODE_ATTACK,
    MODE_HOLD,
    MODE_RELEASE
};

<<init>>=
env->mode = MODE_ZERO;

Previous Output

The output from the previous sample is stored.

<<sk_env>>=
SKFLT prev;

<<init>>=
env->prev = 0;

Parameters

Attack, release, and hold time utilize parameter caching in order to reduce needless computations.

Attack Time (in tau units)

Set the attack time with sk_env_attack.

<<funcdefs>>=
void sk_env_attack(sk_env *env, SKFLT atk);

<<funcs>>=
void sk_env_attack(sk_env *env, SKFLT atk)
{
    env->atk = atk;
}

<<sk_env>>=
SKFLT atk;
SKFLT patk;

Set to arbitrary default value.

<<init>>=
sk_env_attack(env, 0.1);
env->patk = -1;

Release Time (in tau units)

Set the release time with sk_env_release.

<<funcdefs>>=
void sk_env_release(sk_env *env, SKFLT rel);

<<funcs>>=
void sk_env_release(sk_env *env, SKFLT rel)
{
    env->rel= rel;
}

<<sk_env>>=
SKFLT rel;
SKFLT prel;

Set to arbitrary default value.

<<init>>=
sk_env_release(env, 0.1);
env->prel= -1;

Hold Time (in seconds)

The hold time is set with sk_env_hold.

<<funcdefs>>=
void sk_env_hold(sk_env *env, SKFLT hold);

<<funcs>>=
void sk_env_hold(sk_env *env, SKFLT hold)
{
    env->hold = hold;
}

<<sk_env>>=
SKFLT hold;
SKFLT phold;

<<init>>=
sk_env_hold(env, 0.1);
env->phold = -1;

A Decent Epsilon Value

To break out of attack mode, and to have a clean silence after release mode, a very small value known as an epsilonwill be used. This value should be small enough to be perceptually perfect, and large enough to be numerically stable.

The ideal epsilon value is around 5e-8. High-quality tends to be 24-bit, so anything less than 1/2^24 can be ignored, which is roughly 5e-8(rounded down), which I think should be reasonable enough for 32-bit floating-point numbers.

<<macros>>=
#define EPS 5e-8

Computation

Computation is done with sk_env_tick. It takes in one input value, which expects to be a trigger.

<<funcdefs>>=
SKFLT sk_env_tick(sk_env *env, SKFLT trig);

<<funcs>>=
SKFLT sk_env_tick(sk_env *env, SKFLT trig)
{
    SKFLT out;
    out = 0;

    <<check_for_trigger>>

    switch (env->mode) {
        <<zero_mode>>
        <<attack_mode>>
        <<hold_mode>>
        <<release_mode>>
        default:
            break;
    }
    return out;
}

The envelope will wait for a trigger. A trigger at any point will cause a retrigger. Some work will be done to write in behavior that will prevent clicks from happening.

When the trigger occurs, the state is set to attack, and the gate signal is turned on. The attack time parameters are updated at this point, and the filter is configured to use the attack time parameters.

<<check_for_trigger>>=
if (trig != 0) {
    env->mode = MODE_ATTACK;

    if (env->patk != env->atk) {
        env->patk = env->atk;
        env->atk_env = exp(-1.0 / (env->atk * env->sr));
    }
}

In attack mode, the on-gate signal is put through the filter. The difference between the current and previous samples is measured. If it falls under an epsilon value, the current value is held and it set to be hold mode.

Figuring out when the attack is done is tricky. Using threshold detection has proven to be unreliable with different attack times, so the delta technique used below was used instead.

<<attack_mode>>=
case MODE_ATTACK: {
    out = env->atk_env*env->prev + (1.0 - env->atk_env);

    if ((out - env->prev) <= EPS) {
        env->mode = MODE_HOLD;
        env->timer = 0;

        if (env->phold != env->hold) {
            if (env->hold <= 0) {
                env->inc = 1.0;
            } else {
                env->phold = env->hold;
                env->inc = 1.0 / (env->hold * env->sr);
            }
        }
    }

    env->prev = out;
    break;
}

The timer is stored in a normalized range, which allows the hold time to be adjustable while it is in hold mode. During the period in hold mode, the output signal will be the last returned value of the attack envelope.

When timer reaches the end, the envelope goes into release mode.

<<hold_mode>>=
case MODE_HOLD: {
    out = env->prev;
    env->timer += env->inc;

    if (env->timer >= 1.0) {
        env->mode = MODE_RELEASE;

        if (env->prel != env->rel) {
            env->prel = env->rel;
            env->rel_env = exp(-1 / (env->rel * env->sr));
        }
    }
    break;
}

Release mode is very similar to to attack mode, except that it uses release time coefficients and the input signal is a off-gate.

<<release_mode>>=
case MODE_RELEASE: {
    out = env->rel_env*env->prev;
    env->prev = out;

    if (out <= EPS) {
       env->mode = MODE_ZERO;
    }
    break;
}

The envelope will remain in release mode until the output signal goes below the epsilon threshold. At this point, the envelope will go into zero mode, where no computation happens and zero is returned.

<<zero_mode>>=
case MODE_ZERO:
    break;